@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,4538 @@
1
+ /**
2
+ * Chevrotain CstParser + imperative CST→AST visitor for the Bridge DSL.
3
+ *
4
+ * Drop-in replacement for the regex-based `parseBridge()` in bridge-format.ts.
5
+ * Produces the *exact same* AST types (`Instruction[]`).
6
+ */
7
+ import { CstParser } from "chevrotain";
8
+ import { allTokens, Identifier, VersionKw, ToolKw, BridgeKw, DefineKw, ConstKw, WithKw, AsKw, FromKw, InputKw, OutputKw, ContextKw, OnKw, ErrorKw, Arrow, ForceKw, AliasKw, AndKw, OrKw, NotKw, ThrowKw, PanicKw, ContinueKw, BreakKw, NullCoalesce, ErrorCoalesce, SafeNav, CatchKw, LParen, RParen, LCurly, RCurly, LSquare, RSquare, Equals, Dot, Colon, Comma, StringLiteral, NumberLiteral, PathToken, TrueLiteral, FalseLiteral, NullLiteral, Star, Slash, Plus, Minus, GreaterEqual, LessEqual, DoubleEquals, NotEquals, GreaterThan, LessThan, QuestionMark, BridgeLexer, } from "./lexer.js";
9
+ import { SELF_MODULE } from "@stackables/bridge-core";
10
+ // ── Reserved-word guards (mirroring the regex parser) ──────────────────────
11
+ const RESERVED_KEYWORDS = new Set([
12
+ "bridge",
13
+ "with",
14
+ "as",
15
+ "from",
16
+ "const",
17
+ "tool",
18
+ "version",
19
+ "define",
20
+ "alias",
21
+ "throw",
22
+ "panic",
23
+ "continue",
24
+ "break",
25
+ ]);
26
+ const SOURCE_IDENTIFIERS = new Set(["input", "output", "context"]);
27
+ function assertNotReserved(name, lineNum, label) {
28
+ if (RESERVED_KEYWORDS.has(name.toLowerCase())) {
29
+ throw new Error(`Line ${lineNum}: "${name}" is a reserved keyword and cannot be used as a ${label}`);
30
+ }
31
+ if (SOURCE_IDENTIFIERS.has(name.toLowerCase())) {
32
+ throw new Error(`Line ${lineNum}: "${name}" is a reserved source identifier and cannot be used as a ${label}`);
33
+ }
34
+ }
35
+ // ═══════════════════════════════════════════════════════════════════════════
36
+ // Grammar (CstParser)
37
+ // ═══════════════════════════════════════════════════════════════════════════
38
+ class BridgeParser extends CstParser {
39
+ constructor(opts = {}) {
40
+ super(allTokens, {
41
+ recoveryEnabled: opts.recovery ?? false,
42
+ maxLookahead: 4,
43
+ });
44
+ this.performSelfAnalysis();
45
+ }
46
+ // ── Top-level ──────────────────────────────────────────────────────────
47
+ program = this.RULE("program", () => {
48
+ this.SUBRULE(this.versionDecl);
49
+ this.MANY(() => {
50
+ this.OR([
51
+ { ALT: () => this.SUBRULE(this.toolBlock) },
52
+ { ALT: () => this.SUBRULE(this.bridgeBlock) },
53
+ { ALT: () => this.SUBRULE(this.defineBlock) },
54
+ { ALT: () => this.SUBRULE(this.constDecl) },
55
+ ]);
56
+ });
57
+ });
58
+ /** version 1.5 */
59
+ versionDecl = this.RULE("versionDecl", () => {
60
+ this.CONSUME(VersionKw);
61
+ this.CONSUME(NumberLiteral, { LABEL: "ver" });
62
+ });
63
+ // ── Tool block ─────────────────────────────────────────────────────────
64
+ toolBlock = this.RULE("toolBlock", () => {
65
+ this.CONSUME(ToolKw);
66
+ this.SUBRULE(this.dottedName, { LABEL: "toolName" });
67
+ this.CONSUME(FromKw);
68
+ this.SUBRULE2(this.dottedName, { LABEL: "toolSource" });
69
+ this.OPTION(() => {
70
+ this.CONSUME(LCurly);
71
+ this.MANY(() => this.SUBRULE(this.toolBodyLine));
72
+ this.CONSUME(RCurly);
73
+ });
74
+ });
75
+ /**
76
+ * A single line inside a tool block.
77
+ *
78
+ * Ambiguity fix: `.target = value` and `.target <- source` share the
79
+ * prefix `Dot dottedPath`, so we merge them into one alternative that
80
+ * parses the prefix then branches on `=` vs `<-`.
81
+ *
82
+ * `on error` and `with` have distinct first tokens so they stay separate.
83
+ */
84
+ toolBodyLine = this.RULE("toolBodyLine", () => {
85
+ this.OR([
86
+ { ALT: () => this.SUBRULE(this.toolOnError) },
87
+ { ALT: () => this.SUBRULE(this.toolWithDecl) },
88
+ { ALT: () => this.SUBRULE(this.toolWire) }, // merged constant + pull
89
+ ]);
90
+ });
91
+ /**
92
+ * Tool wire (merged): .target = value | .target <- source
93
+ *
94
+ * Parses the common prefix `.dottedPath` then branches on operator.
95
+ */
96
+ toolWire = this.RULE("toolWire", () => {
97
+ this.CONSUME(Dot);
98
+ this.SUBRULE(this.dottedPath, { LABEL: "target" });
99
+ this.OR([
100
+ {
101
+ ALT: () => {
102
+ this.CONSUME(Equals, { LABEL: "equalsOp" });
103
+ this.SUBRULE(this.bareValue, { LABEL: "value" });
104
+ },
105
+ },
106
+ {
107
+ ALT: () => {
108
+ this.CONSUME(Arrow, { LABEL: "arrowOp" });
109
+ this.SUBRULE(this.dottedName, { LABEL: "source" });
110
+ },
111
+ },
112
+ ]);
113
+ });
114
+ /** on error = <value> | on error <- <source> */
115
+ toolOnError = this.RULE("toolOnError", () => {
116
+ this.CONSUME(OnKw);
117
+ this.CONSUME(ErrorKw);
118
+ this.OR([
119
+ {
120
+ ALT: () => {
121
+ this.CONSUME(Equals, { LABEL: "equalsOp" });
122
+ this.SUBRULE(this.jsonValue, { LABEL: "errorValue" });
123
+ },
124
+ },
125
+ {
126
+ ALT: () => {
127
+ this.CONSUME(Arrow, { LABEL: "arrowOp" });
128
+ this.SUBRULE(this.dottedName, { LABEL: "errorSource" });
129
+ },
130
+ },
131
+ ]);
132
+ });
133
+ /** with context [as alias] | with const [as alias] | with <tool> as <alias> */
134
+ toolWithDecl = this.RULE("toolWithDecl", () => {
135
+ this.CONSUME(WithKw);
136
+ this.OR([
137
+ {
138
+ ALT: () => {
139
+ this.CONSUME(ContextKw, { LABEL: "contextKw" });
140
+ this.OPTION(() => {
141
+ this.CONSUME(AsKw);
142
+ this.SUBRULE(this.nameToken, { LABEL: "alias" });
143
+ });
144
+ },
145
+ },
146
+ {
147
+ ALT: () => {
148
+ this.CONSUME(ConstKw, { LABEL: "constKw" });
149
+ this.OPTION2(() => {
150
+ this.CONSUME2(AsKw);
151
+ this.SUBRULE2(this.nameToken, { LABEL: "constAlias" });
152
+ });
153
+ },
154
+ },
155
+ {
156
+ // General tool reference — GATE excludes keywords handled above
157
+ GATE: () => {
158
+ const la = this.LA(1);
159
+ return la.tokenType !== ContextKw && la.tokenType !== ConstKw;
160
+ },
161
+ ALT: () => {
162
+ this.SUBRULE(this.dottedName, { LABEL: "toolName" });
163
+ this.CONSUME3(AsKw);
164
+ this.SUBRULE3(this.nameToken, { LABEL: "toolAlias" });
165
+ },
166
+ },
167
+ ]);
168
+ });
169
+ // ── Bridge block ───────────────────────────────────────────────────────
170
+ bridgeBlock = this.RULE("bridgeBlock", () => {
171
+ this.CONSUME(BridgeKw);
172
+ this.SUBRULE(this.nameToken, { LABEL: "typeName" });
173
+ this.CONSUME(Dot);
174
+ this.SUBRULE2(this.nameToken, { LABEL: "fieldName" });
175
+ this.OR([
176
+ {
177
+ // Passthrough shorthand: bridge Type.field with <name>
178
+ ALT: () => {
179
+ this.CONSUME(WithKw, { LABEL: "passthroughWith" });
180
+ this.SUBRULE(this.dottedName, { LABEL: "passthroughName" });
181
+ },
182
+ },
183
+ {
184
+ // Full bridge block: bridge Type.field { ... }
185
+ ALT: () => {
186
+ this.CONSUME(LCurly);
187
+ this.MANY(() => this.SUBRULE(this.bridgeBodyLine));
188
+ this.CONSUME(RCurly);
189
+ },
190
+ },
191
+ ]);
192
+ });
193
+ /**
194
+ * A line inside a bridge/define body.
195
+ *
196
+ * Ambiguity fix: `target = value` and `target <- source` share the prefix
197
+ * `addressPath`, so they're merged into `bridgeWire`.
198
+ * `with` declarations start with WithKw and are unambiguous.
199
+ * `alias` declarations start with AliasKw and are unambiguous.
200
+ */
201
+ bridgeBodyLine = this.RULE("bridgeBodyLine", () => {
202
+ this.OR([
203
+ { ALT: () => this.SUBRULE(this.bridgeNodeAlias) },
204
+ { ALT: () => this.SUBRULE(this.bridgeWithDecl) },
205
+ { ALT: () => this.SUBRULE(this.bridgeForce) },
206
+ { ALT: () => this.SUBRULE(this.bridgeWire) }, // merged constant + pull
207
+ ]);
208
+ });
209
+ /**
210
+ * Node alias at bridge body level:
211
+ * alias <sourceExpr> as <name>
212
+ *
213
+ * Creates a local __local binding that caches the result of the source
214
+ * expression. Subsequent wires can reference the alias as a handle.
215
+ */
216
+ bridgeNodeAlias = this.RULE("bridgeNodeAlias", () => {
217
+ this.CONSUME(AliasKw);
218
+ this.OR([
219
+ {
220
+ // String literal as source: alias "..." [op operand]* [? then : else] as name
221
+ ALT: () => {
222
+ this.CONSUME(StringLiteral, { LABEL: "aliasStringSource" });
223
+ // Optional expression chain after string literal
224
+ this.MANY3(() => {
225
+ this.SUBRULE2(this.exprOperator, { LABEL: "aliasStringExprOp" });
226
+ this.SUBRULE2(this.exprOperand, { LABEL: "aliasStringExprRight" });
227
+ });
228
+ // Optional ternary after string literal expression
229
+ this.OPTION5(() => {
230
+ this.CONSUME2(QuestionMark, { LABEL: "aliasStringTernaryOp" });
231
+ this.SUBRULE3(this.ternaryBranch, {
232
+ LABEL: "aliasStringThenBranch",
233
+ });
234
+ this.CONSUME2(Colon, { LABEL: "aliasStringTernaryColon" });
235
+ this.SUBRULE4(this.ternaryBranch, {
236
+ LABEL: "aliasStringElseBranch",
237
+ });
238
+ });
239
+ },
240
+ },
241
+ {
242
+ // [not] (parenExpr | sourceExpr) [op operand]* [? then : else] as name
243
+ ALT: () => {
244
+ this.OPTION3(() => {
245
+ this.CONSUME(NotKw, { LABEL: "aliasNotPrefix" });
246
+ });
247
+ this.OR2([
248
+ {
249
+ ALT: () => {
250
+ this.SUBRULE(this.parenExpr, { LABEL: "aliasFirstParen" });
251
+ },
252
+ },
253
+ {
254
+ ALT: () => {
255
+ this.SUBRULE(this.sourceExpr, { LABEL: "nodeAliasSource" });
256
+ },
257
+ },
258
+ ]);
259
+ // Optional expression chain: op operand pairs
260
+ this.MANY2(() => {
261
+ this.SUBRULE(this.exprOperator, { LABEL: "aliasExprOp" });
262
+ this.SUBRULE(this.exprOperand, { LABEL: "aliasExprRight" });
263
+ });
264
+ // Optional ternary: ? thenBranch : elseBranch
265
+ this.OPTION4(() => {
266
+ this.CONSUME(QuestionMark, { LABEL: "aliasTernaryOp" });
267
+ this.SUBRULE(this.ternaryBranch, { LABEL: "aliasThenBranch" });
268
+ this.CONSUME(Colon, { LABEL: "aliasTernaryColon" });
269
+ this.SUBRULE2(this.ternaryBranch, { LABEL: "aliasElseBranch" });
270
+ });
271
+ },
272
+ },
273
+ ]);
274
+ // || coalesce chain
275
+ this.MANY(() => {
276
+ this.CONSUME(NullCoalesce);
277
+ this.SUBRULE(this.coalesceAlternative, { LABEL: "aliasNullAlt" });
278
+ });
279
+ // ?? nullish fallback
280
+ this.OPTION(() => {
281
+ this.CONSUME(ErrorCoalesce);
282
+ this.SUBRULE2(this.coalesceAlternative, { LABEL: "aliasNullishAlt" });
283
+ });
284
+ // catch error fallback
285
+ this.OPTION2(() => {
286
+ this.CONSUME(CatchKw);
287
+ this.SUBRULE3(this.coalesceAlternative, { LABEL: "aliasCatchAlt" });
288
+ });
289
+ this.CONSUME(AsKw);
290
+ this.SUBRULE(this.nameToken, { LABEL: "nodeAliasName" });
291
+ });
292
+ /** force <handle> [?? null] */
293
+ bridgeForce = this.RULE("bridgeForce", () => {
294
+ this.CONSUME(ForceKw);
295
+ this.SUBRULE(this.nameToken, { LABEL: "forcedHandle" });
296
+ this.OPTION(() => {
297
+ this.CONSUME(CatchKw, { LABEL: "forceCatchKw" });
298
+ this.CONSUME(NullLiteral, { LABEL: "forceNullFallback" });
299
+ });
300
+ });
301
+ /** with input/output/context/const/tool [as handle] */
302
+ bridgeWithDecl = this.RULE("bridgeWithDecl", () => {
303
+ this.CONSUME(WithKw);
304
+ this.OR([
305
+ {
306
+ ALT: () => {
307
+ this.CONSUME(InputKw, { LABEL: "inputKw" });
308
+ this.OPTION(() => {
309
+ this.CONSUME(AsKw);
310
+ this.SUBRULE(this.nameToken, { LABEL: "inputAlias" });
311
+ });
312
+ },
313
+ },
314
+ {
315
+ ALT: () => {
316
+ this.CONSUME(OutputKw, { LABEL: "outputKw" });
317
+ this.OPTION2(() => {
318
+ this.CONSUME2(AsKw);
319
+ this.SUBRULE2(this.nameToken, { LABEL: "outputAlias" });
320
+ });
321
+ },
322
+ },
323
+ {
324
+ ALT: () => {
325
+ this.CONSUME(ContextKw, { LABEL: "contextKw" });
326
+ this.OPTION3(() => {
327
+ this.CONSUME3(AsKw);
328
+ this.SUBRULE3(this.nameToken, { LABEL: "contextAlias" });
329
+ });
330
+ },
331
+ },
332
+ {
333
+ ALT: () => {
334
+ this.CONSUME(ConstKw, { LABEL: "constKw" });
335
+ this.OPTION4(() => {
336
+ this.CONSUME4(AsKw);
337
+ this.SUBRULE4(this.nameToken, { LABEL: "constAlias" });
338
+ });
339
+ },
340
+ },
341
+ {
342
+ // tool or define: with <name> [as <handle>]
343
+ // GATE excludes keywords handled by specific alternatives above
344
+ GATE: () => {
345
+ const la = this.LA(1);
346
+ return (la.tokenType !== InputKw &&
347
+ la.tokenType !== OutputKw &&
348
+ la.tokenType !== ContextKw &&
349
+ la.tokenType !== ConstKw);
350
+ },
351
+ ALT: () => {
352
+ this.SUBRULE(this.dottedName, { LABEL: "refName" });
353
+ this.OPTION5(() => {
354
+ this.CONSUME5(AsKw);
355
+ this.SUBRULE5(this.nameToken, { LABEL: "refAlias" });
356
+ });
357
+ },
358
+ },
359
+ ]);
360
+ });
361
+ /**
362
+ * Merged bridge wire (constant, pull/expression, or path scoping block):
363
+ * target = value
364
+ * target <-[!] sourceExpr [op operand]* [[] as iter { ...elements... }]
365
+ * [|| alt]* [?? fallback]
366
+ * target { .field <- source | .field = value | .field { ... } }
367
+ */
368
+ bridgeWire = this.RULE("bridgeWire", () => {
369
+ this.SUBRULE(this.addressPath, { LABEL: "target" });
370
+ this.OR([
371
+ {
372
+ // Constant wire: target = value
373
+ ALT: () => {
374
+ this.CONSUME(Equals, { LABEL: "equalsOp" });
375
+ this.SUBRULE(this.bareValue, { LABEL: "constValue" });
376
+ },
377
+ },
378
+ {
379
+ // Pull wire: target <-[!] sourceExpr [op operand]* [modifiers]
380
+ ALT: () => {
381
+ this.CONSUME(Arrow, { LABEL: "arrow" });
382
+ this.OR2([
383
+ {
384
+ // String literal as source (template or plain): target <- "..."
385
+ ALT: () => {
386
+ this.CONSUME(StringLiteral, { LABEL: "stringSource" });
387
+ },
388
+ },
389
+ {
390
+ // Normal source expression with optional `not` prefix
391
+ ALT: () => {
392
+ this.OPTION4(() => {
393
+ this.CONSUME(NotKw, { LABEL: "notPrefix" });
394
+ });
395
+ this.OR6([
396
+ {
397
+ // Parenthesized sub-expression as first source
398
+ ALT: () => {
399
+ this.SUBRULE(this.parenExpr, { LABEL: "firstParenExpr" });
400
+ },
401
+ },
402
+ {
403
+ ALT: () => {
404
+ this.SUBRULE(this.sourceExpr, { LABEL: "firstSource" });
405
+ },
406
+ },
407
+ ]);
408
+ // Optional expression chain: operator + operand, repeatable
409
+ this.MANY2(() => {
410
+ this.SUBRULE(this.exprOperator, { LABEL: "exprOp" });
411
+ this.SUBRULE(this.exprOperand, { LABEL: "exprRight" });
412
+ });
413
+ // Optional ternary: ? thenBranch : elseBranch
414
+ this.OPTION3(() => {
415
+ this.CONSUME(QuestionMark, { LABEL: "ternaryOp" });
416
+ this.SUBRULE(this.ternaryBranch, { LABEL: "thenBranch" });
417
+ this.CONSUME(Colon, { LABEL: "ternaryColon" });
418
+ this.SUBRULE2(this.ternaryBranch, { LABEL: "elseBranch" });
419
+ });
420
+ },
421
+ },
422
+ ]);
423
+ // Optional array mapping: [] as <iter> { ... }
424
+ this.OPTION(() => this.SUBRULE(this.arrayMapping));
425
+ // || coalesce chain
426
+ this.MANY(() => {
427
+ this.CONSUME(NullCoalesce);
428
+ this.SUBRULE(this.coalesceAlternative, { LABEL: "nullAlt" });
429
+ });
430
+ // ?? nullish fallback
431
+ this.OPTION2(() => {
432
+ this.CONSUME(ErrorCoalesce);
433
+ this.SUBRULE2(this.coalesceAlternative, { LABEL: "nullishAlt" });
434
+ });
435
+ // catch error fallback
436
+ this.OPTION5(() => {
437
+ this.CONSUME(CatchKw);
438
+ this.SUBRULE3(this.coalesceAlternative, { LABEL: "catchAlt" });
439
+ });
440
+ },
441
+ },
442
+ {
443
+ // Path scoping block: target { .field <- source | .field = value | .field { ... } | alias ... as ... }
444
+ ALT: () => {
445
+ this.CONSUME(LCurly, { LABEL: "scopeBlock" });
446
+ this.MANY3(() => this.OR3([
447
+ {
448
+ ALT: () => this.SUBRULE(this.bridgeNodeAlias, { LABEL: "scopeAlias" }),
449
+ },
450
+ { ALT: () => this.SUBRULE(this.pathScopeLine) },
451
+ ]));
452
+ this.CONSUME(RCurly);
453
+ },
454
+ },
455
+ ]);
456
+ });
457
+ /** [] as <iter> { ...element lines / local with-bindings... } */
458
+ arrayMapping = this.RULE("arrayMapping", () => {
459
+ this.CONSUME(LSquare);
460
+ this.CONSUME(RSquare);
461
+ this.CONSUME(AsKw);
462
+ this.SUBRULE(this.nameToken, { LABEL: "iterName" });
463
+ this.CONSUME(LCurly);
464
+ this.MANY(() => this.OR([
465
+ { ALT: () => this.SUBRULE(this.elementWithDecl) },
466
+ { ALT: () => this.SUBRULE(this.elementLine) },
467
+ ]));
468
+ this.CONSUME(RCurly);
469
+ });
470
+ /**
471
+ * Block-scoped binding inside array mapping:
472
+ * alias <sourceExpr> as <name>
473
+ * Evaluates the source once per element and binds the result to <name>.
474
+ */
475
+ elementWithDecl = this.RULE("elementWithDecl", () => {
476
+ this.CONSUME(AliasKw);
477
+ this.SUBRULE(this.sourceExpr, { LABEL: "elemWithSource" });
478
+ this.CONSUME(AsKw);
479
+ this.SUBRULE(this.nameToken, { LABEL: "elemWithAlias" });
480
+ });
481
+ /**
482
+ * Element line inside array mapping:
483
+ * .field = value
484
+ * .field <- source [op operand]*
485
+ * .field <- source [|| ...] [?? ...]
486
+ * .field <- source[] as iter { ...nested elements... } (nested array)
487
+ */
488
+ elementLine = this.RULE("elementLine", () => {
489
+ this.CONSUME(Dot);
490
+ this.SUBRULE(this.dottedPath, { LABEL: "elemTarget" });
491
+ this.OR([
492
+ {
493
+ ALT: () => {
494
+ this.CONSUME(Equals, { LABEL: "elemEquals" });
495
+ this.SUBRULE(this.bareValue, { LABEL: "elemValue" });
496
+ },
497
+ },
498
+ {
499
+ ALT: () => {
500
+ this.CONSUME(Arrow, { LABEL: "elemArrow" });
501
+ this.OR2([
502
+ {
503
+ // String literal as source (template or plain): .field <- "..."
504
+ ALT: () => {
505
+ this.CONSUME(StringLiteral, { LABEL: "elemStringSource" });
506
+ },
507
+ },
508
+ {
509
+ // Normal source expression with optional `not` prefix
510
+ ALT: () => {
511
+ this.OPTION4(() => {
512
+ this.CONSUME(NotKw, { LABEL: "elemNotPrefix" });
513
+ });
514
+ this.OR4([
515
+ {
516
+ ALT: () => {
517
+ this.SUBRULE2(this.parenExpr, {
518
+ LABEL: "elemFirstParenExpr",
519
+ });
520
+ },
521
+ },
522
+ {
523
+ ALT: () => {
524
+ this.SUBRULE(this.sourceExpr, { LABEL: "elemSource" });
525
+ },
526
+ },
527
+ ]);
528
+ // Optional expression chain
529
+ this.MANY2(() => {
530
+ this.SUBRULE(this.exprOperator, { LABEL: "elemExprOp" });
531
+ this.SUBRULE(this.exprOperand, { LABEL: "elemExprRight" });
532
+ });
533
+ // Optional ternary: ? thenBranch : elseBranch
534
+ this.OPTION3(() => {
535
+ this.CONSUME(QuestionMark, { LABEL: "elemTernaryOp" });
536
+ this.SUBRULE(this.ternaryBranch, { LABEL: "elemThenBranch" });
537
+ this.CONSUME(Colon, { LABEL: "elemTernaryColon" });
538
+ this.SUBRULE2(this.ternaryBranch, {
539
+ LABEL: "elemElseBranch",
540
+ });
541
+ });
542
+ },
543
+ },
544
+ ]);
545
+ // Optional nested array mapping: [] as <iter> { ... }
546
+ this.OPTION2(() => this.SUBRULE(this.arrayMapping, { LABEL: "nestedArrayMapping" }));
547
+ // || coalesce chain (only when no nested array mapping)
548
+ this.MANY(() => {
549
+ this.CONSUME(NullCoalesce);
550
+ this.SUBRULE(this.coalesceAlternative, { LABEL: "elemNullAlt" });
551
+ });
552
+ // ?? nullish fallback
553
+ this.OPTION(() => {
554
+ this.CONSUME(ErrorCoalesce);
555
+ this.SUBRULE2(this.coalesceAlternative, {
556
+ LABEL: "elemNullishAlt",
557
+ });
558
+ });
559
+ // catch error fallback
560
+ this.OPTION5(() => {
561
+ this.CONSUME(CatchKw);
562
+ this.SUBRULE3(this.coalesceAlternative, { LABEL: "elemCatchAlt" });
563
+ });
564
+ },
565
+ },
566
+ {
567
+ // Path scope block: .field { .subField <- source | .subField = value | ... }
568
+ ALT: () => {
569
+ this.CONSUME(LCurly, { LABEL: "elemScopeBlock" });
570
+ this.MANY3(() => this.SUBRULE(this.pathScopeLine, { LABEL: "elemScopeLine" }));
571
+ this.CONSUME(RCurly);
572
+ },
573
+ },
574
+ ]);
575
+ });
576
+ /**
577
+ * Path scope line: .target = value | .target <- source | .target { ... }
578
+ *
579
+ * Used inside path scoping blocks to build deeply nested objects
580
+ * without repeating the full target path. Supports the same source
581
+ * syntax as bridge wires (pipes, expressions, ternary, fallbacks).
582
+ */
583
+ pathScopeLine = this.RULE("pathScopeLine", () => {
584
+ this.CONSUME(Dot);
585
+ this.SUBRULE(this.dottedPath, { LABEL: "scopeTarget" });
586
+ this.OR([
587
+ {
588
+ // Constant: .field = value
589
+ ALT: () => {
590
+ this.CONSUME(Equals, { LABEL: "scopeEquals" });
591
+ this.SUBRULE(this.bareValue, { LABEL: "scopeValue" });
592
+ },
593
+ },
594
+ {
595
+ // Pull wire: .field <- source [modifiers]
596
+ ALT: () => {
597
+ this.CONSUME(Arrow, { LABEL: "scopeArrow" });
598
+ this.OR2([
599
+ {
600
+ ALT: () => {
601
+ this.CONSUME(StringLiteral, { LABEL: "scopeStringSource" });
602
+ },
603
+ },
604
+ {
605
+ ALT: () => {
606
+ this.OPTION3(() => {
607
+ this.CONSUME(NotKw, { LABEL: "scopeNotPrefix" });
608
+ });
609
+ this.OR5([
610
+ {
611
+ ALT: () => {
612
+ this.SUBRULE3(this.parenExpr, {
613
+ LABEL: "scopeFirstParenExpr",
614
+ });
615
+ },
616
+ },
617
+ {
618
+ ALT: () => {
619
+ this.SUBRULE(this.sourceExpr, { LABEL: "scopeSource" });
620
+ },
621
+ },
622
+ ]);
623
+ this.MANY(() => {
624
+ this.SUBRULE(this.exprOperator, { LABEL: "scopeExprOp" });
625
+ this.SUBRULE(this.exprOperand, { LABEL: "scopeExprRight" });
626
+ });
627
+ this.OPTION(() => {
628
+ this.CONSUME(QuestionMark, { LABEL: "scopeTernaryOp" });
629
+ this.SUBRULE(this.ternaryBranch, {
630
+ LABEL: "scopeThenBranch",
631
+ });
632
+ this.CONSUME(Colon, { LABEL: "scopeTernaryColon" });
633
+ this.SUBRULE2(this.ternaryBranch, {
634
+ LABEL: "scopeElseBranch",
635
+ });
636
+ });
637
+ },
638
+ },
639
+ ]);
640
+ // || coalesce chain
641
+ this.MANY2(() => {
642
+ this.CONSUME(NullCoalesce);
643
+ this.SUBRULE(this.coalesceAlternative, { LABEL: "scopeNullAlt" });
644
+ });
645
+ // ?? nullish fallback
646
+ this.OPTION2(() => {
647
+ this.CONSUME(ErrorCoalesce);
648
+ this.SUBRULE2(this.coalesceAlternative, {
649
+ LABEL: "scopeNullishAlt",
650
+ });
651
+ });
652
+ // catch error fallback
653
+ this.OPTION5(() => {
654
+ this.CONSUME(CatchKw);
655
+ this.SUBRULE3(this.coalesceAlternative, { LABEL: "scopeCatchAlt" });
656
+ });
657
+ },
658
+ },
659
+ {
660
+ // Nested scope: .field { ... }
661
+ ALT: () => {
662
+ this.CONSUME(LCurly);
663
+ this.MANY3(() => this.OR3([
664
+ {
665
+ ALT: () => this.SUBRULE(this.bridgeNodeAlias, { LABEL: "scopeAlias" }),
666
+ },
667
+ { ALT: () => this.SUBRULE(this.pathScopeLine) },
668
+ ]));
669
+ this.CONSUME(RCurly);
670
+ },
671
+ },
672
+ ]);
673
+ });
674
+ /** A coalesce alternative: either a JSON literal or a source expression */
675
+ coalesceAlternative = this.RULE("coalesceAlternative", () => {
676
+ // Need to distinguish literal values from source references.
677
+ // Literals start with StringLiteral, NumberLiteral,
678
+ // TrueLiteral, FalseLiteral, NullLiteral, or LCurly (inline JSON object).
679
+ // Sources start with Identifier or keyword-as-name (nameToken) which are
680
+ // handle references.
681
+ //
682
+ // Potential ambiguity: TrueLiteral/FalseLiteral/NullLiteral could be
683
+ // either a literal or a handle name. But the regex parser treats them as
684
+ // literals in || and ?? position (isJsonLiteral check).
685
+ // Identifiers are always source refs. So we use BACKTRACK for safety.
686
+ //
687
+ // Control flow keywords: throw "msg", panic "msg", continue, break
688
+ this.OR([
689
+ {
690
+ ALT: () => {
691
+ this.CONSUME(ThrowKw, { LABEL: "throwKw" });
692
+ this.CONSUME(StringLiteral, { LABEL: "throwMsg" });
693
+ },
694
+ },
695
+ {
696
+ ALT: () => {
697
+ this.CONSUME(PanicKw, { LABEL: "panicKw" });
698
+ this.CONSUME2(StringLiteral, { LABEL: "panicMsg" });
699
+ },
700
+ },
701
+ { ALT: () => this.CONSUME(ContinueKw, { LABEL: "continueKw" }) },
702
+ { ALT: () => this.CONSUME(BreakKw, { LABEL: "breakKw" }) },
703
+ { ALT: () => this.CONSUME3(StringLiteral, { LABEL: "stringLit" }) },
704
+ { ALT: () => this.CONSUME(NumberLiteral, { LABEL: "numberLit" }) },
705
+ { ALT: () => this.CONSUME(TrueLiteral, { LABEL: "trueLit" }) },
706
+ { ALT: () => this.CONSUME(FalseLiteral, { LABEL: "falseLit" }) },
707
+ { ALT: () => this.CONSUME(NullLiteral, { LABEL: "nullLit" }) },
708
+ {
709
+ ALT: () => this.SUBRULE(this.jsonInlineObject, { LABEL: "objectLit" }),
710
+ },
711
+ { ALT: () => this.SUBRULE(this.sourceExpr, { LABEL: "sourceAlt" }) },
712
+ ]);
713
+ });
714
+ // ── Define block ───────────────────────────────────────────────────────
715
+ defineBlock = this.RULE("defineBlock", () => {
716
+ this.CONSUME(DefineKw);
717
+ this.SUBRULE(this.nameToken, { LABEL: "defineName" });
718
+ this.CONSUME(LCurly);
719
+ this.MANY(() => this.SUBRULE(this.bridgeBodyLine));
720
+ this.CONSUME(RCurly);
721
+ });
722
+ // ── Const declaration ──────────────────────────────────────────────────
723
+ /** const <name> = <jsonValue> */
724
+ constDecl = this.RULE("constDecl", () => {
725
+ this.CONSUME(ConstKw);
726
+ this.SUBRULE(this.nameToken, { LABEL: "constName" });
727
+ this.CONSUME(Equals);
728
+ this.SUBRULE(this.jsonValue, { LABEL: "constValue" });
729
+ });
730
+ // ── Shared sub-rules ──────────────────────────────────────────────────
731
+ /** Source expression: [pipe:]*address (pipe chain or simple ref) */
732
+ sourceExpr = this.RULE("sourceExpr", () => {
733
+ this.SUBRULE(this.addressPath, { LABEL: "head" });
734
+ this.MANY(() => {
735
+ this.CONSUME(Colon);
736
+ this.SUBRULE2(this.addressPath, { LABEL: "pipeSegment" });
737
+ });
738
+ });
739
+ /** Expression operator: arithmetic, comparison, or boolean */
740
+ exprOperator = this.RULE("exprOperator", () => {
741
+ this.OR([
742
+ { ALT: () => this.CONSUME(Star, { LABEL: "star" }) },
743
+ { ALT: () => this.CONSUME(Slash, { LABEL: "slash" }) },
744
+ { ALT: () => this.CONSUME(Plus, { LABEL: "plus" }) },
745
+ { ALT: () => this.CONSUME(Minus, { LABEL: "minus" }) },
746
+ { ALT: () => this.CONSUME(DoubleEquals, { LABEL: "doubleEquals" }) },
747
+ { ALT: () => this.CONSUME(NotEquals, { LABEL: "notEquals" }) },
748
+ { ALT: () => this.CONSUME(GreaterEqual, { LABEL: "greaterEqual" }) },
749
+ { ALT: () => this.CONSUME(LessEqual, { LABEL: "lessEqual" }) },
750
+ { ALT: () => this.CONSUME(GreaterThan, { LABEL: "greaterThan" }) },
751
+ { ALT: () => this.CONSUME(LessThan, { LABEL: "lessThan" }) },
752
+ { ALT: () => this.CONSUME(AndKw, { LABEL: "andKw" }) },
753
+ { ALT: () => this.CONSUME(OrKw, { LABEL: "orKw" }) },
754
+ ]);
755
+ });
756
+ /** Expression operand: a source reference, a literal value, or a parenthesized sub-expression */
757
+ exprOperand = this.RULE("exprOperand", () => {
758
+ this.OR([
759
+ { ALT: () => this.CONSUME(NumberLiteral, { LABEL: "numberLit" }) },
760
+ { ALT: () => this.CONSUME(StringLiteral, { LABEL: "stringLit" }) },
761
+ { ALT: () => this.CONSUME(TrueLiteral, { LABEL: "trueLit" }) },
762
+ { ALT: () => this.CONSUME(FalseLiteral, { LABEL: "falseLit" }) },
763
+ { ALT: () => this.CONSUME(NullLiteral, { LABEL: "nullLit" }) },
764
+ { ALT: () => this.SUBRULE(this.parenExpr, { LABEL: "parenExpr" }) },
765
+ { ALT: () => this.SUBRULE(this.sourceExpr, { LABEL: "sourceRef" }) },
766
+ ]);
767
+ });
768
+ /** Parenthesized sub-expression: ( [not] source [op operand]* ) */
769
+ parenExpr = this.RULE("parenExpr", () => {
770
+ this.CONSUME(LParen);
771
+ this.OPTION(() => {
772
+ this.CONSUME(NotKw, { LABEL: "parenNotPrefix" });
773
+ });
774
+ this.SUBRULE(this.sourceExpr, { LABEL: "parenSource" });
775
+ this.MANY(() => {
776
+ this.SUBRULE(this.exprOperator, { LABEL: "parenExprOp" });
777
+ this.SUBRULE(this.exprOperand, { LABEL: "parenExprRight" });
778
+ });
779
+ this.CONSUME(RParen);
780
+ });
781
+ /**
782
+ * Ternary branch: the then/else operand in `cond ? then : else`.
783
+ * Restricted to simple address paths and literals (no pipe chains)
784
+ * to avoid ambiguity with the `:` separator.
785
+ */
786
+ ternaryBranch = this.RULE("ternaryBranch", () => {
787
+ this.OR([
788
+ { ALT: () => this.CONSUME(StringLiteral, { LABEL: "stringLit" }) },
789
+ { ALT: () => this.CONSUME(NumberLiteral, { LABEL: "numberLit" }) },
790
+ { ALT: () => this.CONSUME(TrueLiteral, { LABEL: "trueLit" }) },
791
+ { ALT: () => this.CONSUME(FalseLiteral, { LABEL: "falseLit" }) },
792
+ { ALT: () => this.CONSUME(NullLiteral, { LABEL: "nullLit" }) },
793
+ { ALT: () => this.SUBRULE(this.addressPath, { LABEL: "sourceRef" }) },
794
+ ]);
795
+ });
796
+ /**
797
+ * Address path: a dotted reference with optional array indices.
798
+ * Examples: o.lat, i.name, g.items[0].position.lat, o
799
+ *
800
+ * Note: empty brackets `[]` are NOT consumed here — they belong to
801
+ * the array mapping rule. The GATE on MANY prevents entering when `[`
802
+ * is followed by `]` (empty brackets).
803
+ *
804
+ * Line-boundary guard: stops consuming dots that cross a newline,
805
+ * so `.id` on the next line isn't greedily absorbed as a path continuation
806
+ * inside element blocks.
807
+ */
808
+ addressPath = this.RULE("addressPath", () => {
809
+ this.SUBRULE(this.nameToken, { LABEL: "root" });
810
+ this.MANY({
811
+ GATE: () => {
812
+ const la = this.LA(1);
813
+ if (la.tokenType === Dot || la.tokenType === SafeNav) {
814
+ // Don't continue across a line break — prevents greedy path
815
+ // consumption in multi-line contexts like element blocks.
816
+ // LA(0) gives the last consumed token.
817
+ const prev = this.LA(0);
818
+ if (prev &&
819
+ la.startLine != null &&
820
+ prev.endLine != null &&
821
+ la.startLine > prev.endLine) {
822
+ return false;
823
+ }
824
+ return true;
825
+ }
826
+ if (la.tokenType === LSquare) {
827
+ const la2 = this.LA(2);
828
+ return la2.tokenType === NumberLiteral;
829
+ }
830
+ return false;
831
+ },
832
+ DEF: () => {
833
+ this.OR([
834
+ {
835
+ ALT: () => {
836
+ this.CONSUME(Dot);
837
+ this.SUBRULE(this.pathSegment, { LABEL: "segment" });
838
+ },
839
+ },
840
+ {
841
+ ALT: () => {
842
+ this.CONSUME(SafeNav, { LABEL: "safeNav" });
843
+ this.SUBRULE2(this.pathSegment, { LABEL: "segment" });
844
+ },
845
+ },
846
+ {
847
+ ALT: () => {
848
+ this.CONSUME(LSquare);
849
+ this.CONSUME(NumberLiteral, { LABEL: "arrayIndex" });
850
+ this.CONSUME(RSquare);
851
+ },
852
+ },
853
+ ]);
854
+ },
855
+ });
856
+ });
857
+ /** Segment after a dot: any identifier or keyword usable in a path */
858
+ pathSegment = this.RULE("pathSegment", () => {
859
+ this.OR([
860
+ { ALT: () => this.CONSUME(Identifier) },
861
+ { ALT: () => this.CONSUME(InputKw) },
862
+ { ALT: () => this.CONSUME(OutputKw) },
863
+ { ALT: () => this.CONSUME(ContextKw) },
864
+ { ALT: () => this.CONSUME(ConstKw) },
865
+ { ALT: () => this.CONSUME(ErrorKw) },
866
+ { ALT: () => this.CONSUME(OnKw) },
867
+ { ALT: () => this.CONSUME(FromKw) },
868
+ { ALT: () => this.CONSUME(AsKw) },
869
+ { ALT: () => this.CONSUME(ToolKw) },
870
+ { ALT: () => this.CONSUME(BridgeKw) },
871
+ { ALT: () => this.CONSUME(DefineKw) },
872
+ { ALT: () => this.CONSUME(WithKw) },
873
+ { ALT: () => this.CONSUME(VersionKw) },
874
+ { ALT: () => this.CONSUME(TrueLiteral) },
875
+ { ALT: () => this.CONSUME(FalseLiteral) },
876
+ { ALT: () => this.CONSUME(NullLiteral) },
877
+ { ALT: () => this.CONSUME(AndKw) },
878
+ { ALT: () => this.CONSUME(OrKw) },
879
+ { ALT: () => this.CONSUME(NotKw) },
880
+ ]);
881
+ });
882
+ /** Dotted name: identifier segments separated by dots */
883
+ dottedName = this.RULE("dottedName", () => {
884
+ this.SUBRULE(this.nameToken, { LABEL: "first" });
885
+ this.MANY({
886
+ GATE: () => {
887
+ const la = this.LA(1);
888
+ if (la.tokenType !== Dot)
889
+ return false;
890
+ const prev = this.LA(0);
891
+ if (prev &&
892
+ la.startLine != null &&
893
+ prev.endLine != null &&
894
+ la.startLine > prev.endLine)
895
+ return false;
896
+ return true;
897
+ },
898
+ DEF: () => {
899
+ this.CONSUME(Dot);
900
+ this.SUBRULE2(this.nameToken, { LABEL: "rest" });
901
+ },
902
+ });
903
+ });
904
+ /** Dotted path (within tool block): segments after a leading dot */
905
+ dottedPath = this.RULE("dottedPath", () => {
906
+ this.SUBRULE(this.pathSegment, { LABEL: "first" });
907
+ this.MANY({
908
+ GATE: () => {
909
+ const la = this.LA(1);
910
+ if (la.tokenType !== Dot)
911
+ return false;
912
+ const prev = this.LA(0);
913
+ if (prev &&
914
+ la.startLine != null &&
915
+ prev.endLine != null &&
916
+ la.startLine > prev.endLine)
917
+ return false;
918
+ return true;
919
+ },
920
+ DEF: () => {
921
+ this.CONSUME(Dot);
922
+ this.SUBRULE2(this.pathSegment, { LABEL: "rest" });
923
+ },
924
+ });
925
+ });
926
+ /** A name token: Identifier or certain keywords usable as names.
927
+ * Note: true/false/null are NOT allowed here to avoid ambiguity with
928
+ * literals in coalesceAlternative. They ARE allowed in pathSegment. */
929
+ nameToken = this.RULE("nameToken", () => {
930
+ this.OR([
931
+ { ALT: () => this.CONSUME(Identifier) },
932
+ { ALT: () => this.CONSUME(InputKw) },
933
+ { ALT: () => this.CONSUME(OutputKw) },
934
+ { ALT: () => this.CONSUME(ContextKw) },
935
+ { ALT: () => this.CONSUME(ConstKw) },
936
+ { ALT: () => this.CONSUME(ErrorKw) },
937
+ { ALT: () => this.CONSUME(OnKw) },
938
+ { ALT: () => this.CONSUME(FromKw) },
939
+ { ALT: () => this.CONSUME(AsKw) },
940
+ { ALT: () => this.CONSUME(ToolKw) },
941
+ { ALT: () => this.CONSUME(BridgeKw) },
942
+ { ALT: () => this.CONSUME(DefineKw) },
943
+ { ALT: () => this.CONSUME(WithKw) },
944
+ { ALT: () => this.CONSUME(VersionKw) },
945
+ { ALT: () => this.CONSUME(AliasKw) },
946
+ ]);
947
+ });
948
+ /** Bare value: string, number, path, boolean, null, or unquoted identifier */
949
+ bareValue = this.RULE("bareValue", () => {
950
+ this.OR([
951
+ { ALT: () => this.CONSUME(StringLiteral) },
952
+ { ALT: () => this.CONSUME(NumberLiteral) },
953
+ { ALT: () => this.CONSUME(PathToken) },
954
+ { ALT: () => this.CONSUME(TrueLiteral) },
955
+ { ALT: () => this.CONSUME(FalseLiteral) },
956
+ { ALT: () => this.CONSUME(NullLiteral) },
957
+ { ALT: () => this.CONSUME(Identifier) },
958
+ { ALT: () => this.CONSUME(InputKw) },
959
+ { ALT: () => this.CONSUME(OutputKw) },
960
+ { ALT: () => this.CONSUME(ErrorKw) },
961
+ { ALT: () => this.CONSUME(OnKw) },
962
+ { ALT: () => this.CONSUME(FromKw) },
963
+ { ALT: () => this.CONSUME(AsKw) },
964
+ { ALT: () => this.CONSUME(AliasKw) },
965
+ ]);
966
+ });
967
+ /** JSON value: string, number, boolean, null, object, or array */
968
+ jsonValue = this.RULE("jsonValue", () => {
969
+ this.OR([
970
+ { ALT: () => this.CONSUME(StringLiteral, { LABEL: "string" }) },
971
+ { ALT: () => this.CONSUME(NumberLiteral, { LABEL: "number" }) },
972
+ { ALT: () => this.CONSUME(TrueLiteral, { LABEL: "true" }) },
973
+ { ALT: () => this.CONSUME(FalseLiteral, { LABEL: "false" }) },
974
+ { ALT: () => this.CONSUME(NullLiteral, { LABEL: "null" }) },
975
+ { ALT: () => this.SUBRULE(this.jsonObject, { LABEL: "object" }) },
976
+ { ALT: () => this.SUBRULE(this.jsonArray, { LABEL: "array" }) },
977
+ ]);
978
+ });
979
+ /** JSON object: { ... } — we accept any tokens inside and reconstruct in the visitor */
980
+ jsonObject = this.RULE("jsonObject", () => {
981
+ this.CONSUME(LCurly);
982
+ this.MANY(() => {
983
+ this.OR([
984
+ { ALT: () => this.CONSUME(StringLiteral) },
985
+ { ALT: () => this.CONSUME(NumberLiteral) },
986
+ { ALT: () => this.CONSUME(Colon) },
987
+ { ALT: () => this.CONSUME(Comma) },
988
+ { ALT: () => this.CONSUME(TrueLiteral) },
989
+ { ALT: () => this.CONSUME(FalseLiteral) },
990
+ { ALT: () => this.CONSUME(NullLiteral) },
991
+ { ALT: () => this.CONSUME(Identifier) },
992
+ { ALT: () => this.CONSUME(LSquare) },
993
+ { ALT: () => this.CONSUME(RSquare) },
994
+ { ALT: () => this.CONSUME(Dot) },
995
+ { ALT: () => this.CONSUME(Equals) },
996
+ { ALT: () => this.CONSUME(AndKw) },
997
+ { ALT: () => this.CONSUME(OrKw) },
998
+ { ALT: () => this.CONSUME(NotKw) },
999
+ // Nested objects
1000
+ { ALT: () => this.SUBRULE(this.jsonObject) },
1001
+ ]);
1002
+ });
1003
+ this.CONSUME(RCurly);
1004
+ });
1005
+ /** JSON array: [ ... ] */
1006
+ jsonArray = this.RULE("jsonArray", () => {
1007
+ this.CONSUME(LSquare);
1008
+ this.MANY(() => {
1009
+ this.OR([
1010
+ { ALT: () => this.CONSUME(StringLiteral) },
1011
+ { ALT: () => this.CONSUME(NumberLiteral) },
1012
+ { ALT: () => this.CONSUME(Colon) },
1013
+ { ALT: () => this.CONSUME(Comma) },
1014
+ { ALT: () => this.CONSUME(TrueLiteral) },
1015
+ { ALT: () => this.CONSUME(FalseLiteral) },
1016
+ { ALT: () => this.CONSUME(NullLiteral) },
1017
+ { ALT: () => this.CONSUME(Identifier) },
1018
+ { ALT: () => this.CONSUME(Dot) },
1019
+ { ALT: () => this.SUBRULE(this.jsonObject) },
1020
+ { ALT: () => this.SUBRULE(this.jsonArray) },
1021
+ ]);
1022
+ });
1023
+ this.CONSUME(RSquare);
1024
+ });
1025
+ /** Inline JSON object — used in coalesce alternatives */
1026
+ jsonInlineObject = this.RULE("jsonInlineObject", () => {
1027
+ this.CONSUME(LCurly);
1028
+ this.MANY(() => {
1029
+ this.OR([
1030
+ { ALT: () => this.CONSUME(StringLiteral) },
1031
+ { ALT: () => this.CONSUME(NumberLiteral) },
1032
+ { ALT: () => this.CONSUME(Colon) },
1033
+ { ALT: () => this.CONSUME(Comma) },
1034
+ { ALT: () => this.CONSUME(TrueLiteral) },
1035
+ { ALT: () => this.CONSUME(FalseLiteral) },
1036
+ { ALT: () => this.CONSUME(NullLiteral) },
1037
+ { ALT: () => this.CONSUME(Identifier) },
1038
+ { ALT: () => this.CONSUME(LSquare) },
1039
+ { ALT: () => this.CONSUME(RSquare) },
1040
+ { ALT: () => this.CONSUME(Dot) },
1041
+ { ALT: () => this.CONSUME(Equals) },
1042
+ { ALT: () => this.SUBRULE(this.jsonInlineObject) },
1043
+ ]);
1044
+ });
1045
+ this.CONSUME(RCurly);
1046
+ });
1047
+ }
1048
+ // Singleton parser instances (Chevrotain best practice)
1049
+ // Strict instance: throws on first error (used by parseBridgeChevrotain)
1050
+ const parserInstance = new BridgeParser();
1051
+ // Lenient instance: error recovery enabled (used by parseBridgeDiagnostics)
1052
+ const diagParserInstance = new BridgeParser({ recovery: true });
1053
+ const BRIDGE_VERSION = "1.5";
1054
+ // ═══════════════════════════════════════════════════════════════════════════
1055
+ // Public API
1056
+ // ═══════════════════════════════════════════════════════════════════════════
1057
+ export function parseBridgeChevrotain(text) {
1058
+ return internalParse(text);
1059
+ }
1060
+ /**
1061
+ * Parse a Bridge DSL text and return both the AST and all diagnostics.
1062
+ * Uses Chevrotain's error recovery — always returns a (possibly partial) AST
1063
+ * even when the file has errors. Designed for LSP/IDE use.
1064
+ */
1065
+ export function parseBridgeDiagnostics(text) {
1066
+ const diagnostics = [];
1067
+ // 1. Lex
1068
+ const lexResult = BridgeLexer.tokenize(text);
1069
+ for (const e of lexResult.errors) {
1070
+ diagnostics.push({
1071
+ message: e.message,
1072
+ severity: "error",
1073
+ range: {
1074
+ start: { line: (e.line ?? 1) - 1, character: (e.column ?? 1) - 1 },
1075
+ end: {
1076
+ line: (e.line ?? 1) - 1,
1077
+ character: (e.column ?? 1) - 1 + e.length,
1078
+ },
1079
+ },
1080
+ });
1081
+ }
1082
+ // 2. Parse with Chevrotain error recovery (builds partial CST past errors)
1083
+ diagParserInstance.input = lexResult.tokens;
1084
+ const cst = diagParserInstance.program();
1085
+ for (const e of diagParserInstance.errors) {
1086
+ const t = e.token;
1087
+ diagnostics.push({
1088
+ message: e.message,
1089
+ severity: "error",
1090
+ range: {
1091
+ start: {
1092
+ line: (t.startLine ?? 1) - 1,
1093
+ character: (t.startColumn ?? 1) - 1,
1094
+ },
1095
+ end: {
1096
+ line: (t.endLine ?? t.startLine ?? 1) - 1,
1097
+ character: t.endColumn ?? t.startColumn ?? 1,
1098
+ },
1099
+ },
1100
+ });
1101
+ }
1102
+ // 3. Visit → AST (semantic errors thrown as "Line N: ..." messages)
1103
+ let instructions = [];
1104
+ let startLines = new Map();
1105
+ try {
1106
+ const result = toBridgeAst(cst, []);
1107
+ instructions = result.instructions;
1108
+ startLines = result.startLines;
1109
+ }
1110
+ catch (err) {
1111
+ const msg = String(err?.message ?? err);
1112
+ const m = msg.match(/^Line (\d+):/);
1113
+ const errorLine = m ? parseInt(m[1]) - 1 : 0;
1114
+ diagnostics.push({
1115
+ message: msg.replace(/^Line \d+:\s*/, ""),
1116
+ severity: "error",
1117
+ range: {
1118
+ start: { line: errorLine, character: 0 },
1119
+ end: { line: errorLine, character: 999 },
1120
+ },
1121
+ });
1122
+ }
1123
+ return { instructions, diagnostics, startLines };
1124
+ }
1125
+ function internalParse(text, previousInstructions) {
1126
+ // 1. Lex
1127
+ const lexResult = BridgeLexer.tokenize(text);
1128
+ if (lexResult.errors.length > 0) {
1129
+ const e = lexResult.errors[0];
1130
+ throw new Error(`Line ${e.line}: Unexpected character "${e.message}"`);
1131
+ }
1132
+ // 2. Parse
1133
+ parserInstance.input = lexResult.tokens;
1134
+ const cst = parserInstance.program();
1135
+ if (parserInstance.errors.length > 0) {
1136
+ const e = parserInstance.errors[0];
1137
+ throw new Error(e.message);
1138
+ }
1139
+ // 3. Visit → AST
1140
+ return toBridgeAst(cst, previousInstructions).instructions;
1141
+ }
1142
+ // ═══════════════════════════════════════════════════════════════════════════
1143
+ // CST → AST transformation (imperative visitor)
1144
+ // ═══════════════════════════════════════════════════════════════════════════
1145
+ // ── Token / CST node helpers ────────────────────────────────────────────
1146
+ function sub(node, ruleName) {
1147
+ const nodes = node.children[ruleName];
1148
+ return nodes?.[0];
1149
+ }
1150
+ function subs(node, ruleName) {
1151
+ return node.children[ruleName] ?? [];
1152
+ }
1153
+ function tok(node, tokenName) {
1154
+ const tokens = node.children[tokenName];
1155
+ return tokens?.[0];
1156
+ }
1157
+ function toks(node, tokenName) {
1158
+ return node.children[tokenName] ?? [];
1159
+ }
1160
+ function line(token) {
1161
+ return token?.startLine ?? 0;
1162
+ }
1163
+ /* ── extractNameToken: get string from nameToken CST node ── */
1164
+ function extractNameToken(node) {
1165
+ const c = node.children;
1166
+ for (const key of Object.keys(c)) {
1167
+ const tokens = c[key];
1168
+ if (tokens?.[0])
1169
+ return tokens[0].image;
1170
+ }
1171
+ return "";
1172
+ }
1173
+ /* ── extractDottedName: reassemble from dottedName CST node ── */
1174
+ function extractDottedName(node) {
1175
+ const first = extractNameToken(sub(node, "first"));
1176
+ const rest = subs(node, "rest").map((n) => extractNameToken(n));
1177
+ return [first, ...rest].join(".");
1178
+ }
1179
+ /* ── extractPathSegment: get string from pathSegment ── */
1180
+ function extractPathSegment(node) {
1181
+ for (const key of Object.keys(node.children)) {
1182
+ const tokens = node.children[key];
1183
+ if (tokens?.[0])
1184
+ return tokens[0].image;
1185
+ }
1186
+ return "";
1187
+ }
1188
+ /* ── extractDottedPathStr: reassemble from dottedPath CST node ── */
1189
+ function extractDottedPathStr(node) {
1190
+ const first = extractPathSegment(sub(node, "first"));
1191
+ const rest = subs(node, "rest").map((n) => extractPathSegment(n));
1192
+ return [first, ...rest].join(".");
1193
+ }
1194
+ /* ── extractAddressPath: get root + segments preserving order ── */
1195
+ function extractAddressPath(node) {
1196
+ const root = extractNameToken(sub(node, "root"));
1197
+ const items = [];
1198
+ const safeNavTokens = node.children.safeNav ?? [];
1199
+ const hasSafeNav = safeNavTokens.length > 0;
1200
+ // Also collect Dot token offsets
1201
+ const dotTokens = node.children.Dot ?? [];
1202
+ for (const seg of subs(node, "segment")) {
1203
+ const firstTok = findFirstToken(seg);
1204
+ items.push({
1205
+ offset: firstTok?.startOffset ?? 0,
1206
+ value: extractPathSegment(seg),
1207
+ });
1208
+ }
1209
+ for (const idxTok of toks(node, "arrayIndex")) {
1210
+ if (idxTok.image.includes(".")) {
1211
+ throw new Error(`Line ${idxTok.startLine}: Array indices must be integers, found "${idxTok.image}"`);
1212
+ }
1213
+ items.push({ offset: idxTok.startOffset, value: idxTok.image });
1214
+ }
1215
+ items.sort((a, b) => a.offset - b.offset);
1216
+ // For each segment, determine if it was preceded by a SafeNav token.
1217
+ // Collect all separators (Dot + SafeNav) sorted by offset, then correlate with segments.
1218
+ const allSeps = [
1219
+ ...dotTokens.map((t) => ({ offset: t.startOffset, isSafe: false })),
1220
+ ...safeNavTokens.map((t) => ({ offset: t.startOffset, isSafe: true })),
1221
+ ].sort((a, b) => a.offset - b.offset);
1222
+ // Match separators to segments: each separator precedes the next segment
1223
+ const segmentSafe = [];
1224
+ let rootSafe = false;
1225
+ for (let i = 0; i < items.length; i++) {
1226
+ // Find the separator that immediately precedes this segment
1227
+ const segOffset = items[i].offset;
1228
+ const precedingSep = allSeps.filter((s) => s.offset < segOffset).pop();
1229
+ const isSafe = precedingSep?.isSafe ?? false;
1230
+ if (i === 0) {
1231
+ rootSafe = isSafe;
1232
+ }
1233
+ segmentSafe.push(isSafe);
1234
+ }
1235
+ return {
1236
+ root,
1237
+ segments: items.map((i) => i.value),
1238
+ ...(hasSafeNav ? { safe: true } : {}),
1239
+ ...(rootSafe ? { rootSafe } : {}),
1240
+ ...(segmentSafe.some((s) => s) ? { segmentSafe } : {}),
1241
+ };
1242
+ }
1243
+ function findFirstToken(node) {
1244
+ for (const key of Object.keys(node.children)) {
1245
+ const child = node.children[key];
1246
+ if (Array.isArray(child) && child.length > 0) {
1247
+ const first = child[0];
1248
+ if ("image" in first)
1249
+ return first;
1250
+ if ("children" in first)
1251
+ return findFirstToken(first);
1252
+ }
1253
+ }
1254
+ return undefined;
1255
+ }
1256
+ /* ── parsePath: split "a.b[0].c" → ["a","b","0","c"] ── */
1257
+ function parsePath(text) {
1258
+ const parts = [];
1259
+ for (const segment of text.split(".")) {
1260
+ const match = segment.match(/^([^[]+)(?:\[(\d*)\])?$/);
1261
+ if (match) {
1262
+ parts.push(match[1]);
1263
+ if (match[2] !== undefined && match[2] !== "")
1264
+ parts.push(match[2]);
1265
+ }
1266
+ else {
1267
+ parts.push(segment);
1268
+ }
1269
+ }
1270
+ return parts;
1271
+ }
1272
+ /* ── Collect all tokens recursively from a CST node ── */
1273
+ function collectTokens(node, out) {
1274
+ for (const key of Object.keys(node.children)) {
1275
+ const children = node.children[key];
1276
+ if (!Array.isArray(children))
1277
+ continue;
1278
+ for (const child of children) {
1279
+ if ("image" in child)
1280
+ out.push(child);
1281
+ else if ("children" in child)
1282
+ collectTokens(child, out);
1283
+ }
1284
+ }
1285
+ }
1286
+ function reconstructJson(node) {
1287
+ const tokens = [];
1288
+ collectTokens(node, tokens);
1289
+ tokens.sort((a, b) => a.startOffset - b.startOffset);
1290
+ // Reconstruct with original spacing preserved (using offsets to insert whitespace)
1291
+ if (tokens.length === 0)
1292
+ return "";
1293
+ let result = tokens[0].image;
1294
+ for (let i = 1; i < tokens.length; i++) {
1295
+ const gap = tokens[i].startOffset -
1296
+ (tokens[i - 1].startOffset + tokens[i - 1].image.length);
1297
+ if (gap > 0)
1298
+ result += " ".repeat(gap);
1299
+ result += tokens[i].image;
1300
+ }
1301
+ return result;
1302
+ }
1303
+ /* ── extractBareValue: get the string from a bareValue CST node ── */
1304
+ function extractBareValue(node) {
1305
+ for (const key of Object.keys(node.children)) {
1306
+ const tokens = node.children[key];
1307
+ if (tokens?.[0]) {
1308
+ let val = tokens[0].image;
1309
+ if (val.startsWith('"') && val.endsWith('"'))
1310
+ val = val.slice(1, -1);
1311
+ return val;
1312
+ }
1313
+ }
1314
+ return "";
1315
+ }
1316
+ function parseTemplateString(raw) {
1317
+ // raw is the content between quotes (already stripped of outer quotes)
1318
+ const segs = [];
1319
+ let i = 0;
1320
+ let hasRef = false;
1321
+ let text = "";
1322
+ while (i < raw.length) {
1323
+ if (raw[i] === "\\" && i + 1 < raw.length) {
1324
+ if (raw[i + 1] === "{") {
1325
+ text += "{";
1326
+ i += 2;
1327
+ continue;
1328
+ }
1329
+ // preserve other escapes as-is
1330
+ text += raw[i] + raw[i + 1];
1331
+ i += 2;
1332
+ continue;
1333
+ }
1334
+ if (raw[i] === "{") {
1335
+ const end = raw.indexOf("}", i + 1);
1336
+ if (end === -1) {
1337
+ // unclosed brace — treat as literal text
1338
+ text += raw[i];
1339
+ i++;
1340
+ continue;
1341
+ }
1342
+ const ref = raw.slice(i + 1, end).trim();
1343
+ if (ref.length === 0) {
1344
+ text += "{}";
1345
+ i = end + 1;
1346
+ continue;
1347
+ }
1348
+ if (text.length > 0) {
1349
+ segs.push({ kind: "text", value: text });
1350
+ text = "";
1351
+ }
1352
+ segs.push({ kind: "ref", path: ref });
1353
+ hasRef = true;
1354
+ i = end + 1;
1355
+ continue;
1356
+ }
1357
+ text += raw[i];
1358
+ i++;
1359
+ }
1360
+ if (text.length > 0)
1361
+ segs.push({ kind: "text", value: text });
1362
+ return hasRef ? segs : null;
1363
+ }
1364
+ /* ── extractJsonValue: from a jsonValue CST node ── */
1365
+ function extractJsonValue(node) {
1366
+ const c = node.children;
1367
+ if (c.string)
1368
+ return c.string[0].image; // keep quotes for JSON.parse
1369
+ if (c.number)
1370
+ return c.number[0].image;
1371
+ if (c.integer)
1372
+ return c.integer[0].image;
1373
+ if (c.true)
1374
+ return "true";
1375
+ if (c.false)
1376
+ return "false";
1377
+ if (c.null)
1378
+ return "null";
1379
+ if (c.object)
1380
+ return reconstructJson(c.object[0]);
1381
+ if (c.array)
1382
+ return reconstructJson(c.array[0]);
1383
+ return "";
1384
+ }
1385
+ // ═══════════════════════════════════════════════════════════════════════════
1386
+ // Recursive element-line processor (supports nested array-in-array mapping)
1387
+ // ═══════════════════════════════════════════════════════════════════════════
1388
+ /**
1389
+ * Process element lines inside an array mapping block.
1390
+ * When an element line itself contains a nested `[] as iter { ... }` block,
1391
+ * this function registers the inner iterator and recurses into the nested
1392
+ * element lines, building wires with the correct concatenated paths.
1393
+ */
1394
+ function processElementLines(elemLines, arrayToPath, iterName, bridgeType, bridgeField, wires, arrayIterators, buildSourceExpr, extractCoalesceAlt, desugarExprChain, extractTernaryBranchFn, processLocalBindings, desugarTemplateStringFn, desugarNotFn, resolveParenExprFn) {
1395
+ function extractCoalesceAltIterAware(altNode, lineNum) {
1396
+ const c = altNode.children;
1397
+ if (c.sourceAlt) {
1398
+ const srcNode = c.sourceAlt[0];
1399
+ const headNode = sub(srcNode, "head");
1400
+ if (headNode) {
1401
+ const { root, segments } = extractAddressPath(headNode);
1402
+ const pipeSegs = subs(srcNode, "pipeSegment");
1403
+ if (root === iterName && pipeSegs.length === 0) {
1404
+ return {
1405
+ sourceRef: {
1406
+ module: SELF_MODULE,
1407
+ type: bridgeType,
1408
+ field: bridgeField,
1409
+ element: true,
1410
+ path: segments,
1411
+ },
1412
+ };
1413
+ }
1414
+ }
1415
+ }
1416
+ return extractCoalesceAlt(altNode, lineNum, iterName);
1417
+ }
1418
+ for (const elemLine of elemLines) {
1419
+ const elemC = elemLine.children;
1420
+ const elemLineNum = line(findFirstToken(elemLine));
1421
+ const elemTargetPathStr = extractDottedPathStr(sub(elemLine, "elemTarget"));
1422
+ const elemToPath = [...arrayToPath, ...parsePath(elemTargetPathStr)];
1423
+ if (elemC.elemEquals) {
1424
+ const value = extractBareValue(sub(elemLine, "elemValue"));
1425
+ wires.push({
1426
+ value,
1427
+ to: {
1428
+ module: SELF_MODULE,
1429
+ type: bridgeType,
1430
+ field: bridgeField,
1431
+ element: true,
1432
+ path: elemToPath,
1433
+ },
1434
+ });
1435
+ }
1436
+ else if (elemC.elemArrow) {
1437
+ // ── String source in element context: .field <- "..." ──
1438
+ const elemStrToken = elemC.elemStringSource?.[0];
1439
+ if (elemStrToken && desugarTemplateStringFn) {
1440
+ const raw = elemStrToken.image.slice(1, -1);
1441
+ const segs = parseTemplateString(raw);
1442
+ const elemToRef = {
1443
+ module: SELF_MODULE,
1444
+ type: bridgeType,
1445
+ field: bridgeField,
1446
+ path: elemToPath,
1447
+ };
1448
+ // Process coalesce modifiers
1449
+ let falsyFallback;
1450
+ let falsyControl;
1451
+ const nullAltRefs = [];
1452
+ for (const alt of subs(elemLine, "elemNullAlt")) {
1453
+ const altResult = extractCoalesceAltIterAware(alt, elemLineNum);
1454
+ if ("literal" in altResult) {
1455
+ falsyFallback = altResult.literal;
1456
+ }
1457
+ else if ("control" in altResult) {
1458
+ falsyControl = altResult.control;
1459
+ }
1460
+ else {
1461
+ nullAltRefs.push(altResult.sourceRef);
1462
+ }
1463
+ }
1464
+ let nullishFallback;
1465
+ let nullishControl;
1466
+ let nullishFallbackRef;
1467
+ let nullishFallbackInternalWires = [];
1468
+ const nullishAlt = sub(elemLine, "elemNullishAlt");
1469
+ if (nullishAlt) {
1470
+ const preLen = wires.length;
1471
+ const altResult = extractCoalesceAltIterAware(nullishAlt, elemLineNum);
1472
+ if ("literal" in altResult) {
1473
+ nullishFallback = altResult.literal;
1474
+ }
1475
+ else if ("control" in altResult) {
1476
+ nullishControl = altResult.control;
1477
+ }
1478
+ else {
1479
+ nullishFallbackRef = altResult.sourceRef;
1480
+ nullishFallbackInternalWires = wires.splice(preLen);
1481
+ }
1482
+ }
1483
+ let catchFallback;
1484
+ let catchControl;
1485
+ let catchFallbackRef;
1486
+ let catchFallbackInternalWires = [];
1487
+ const catchAlt = sub(elemLine, "elemCatchAlt");
1488
+ if (catchAlt) {
1489
+ const preLen = wires.length;
1490
+ const altResult = extractCoalesceAltIterAware(catchAlt, elemLineNum);
1491
+ if ("literal" in altResult) {
1492
+ catchFallback = altResult.literal;
1493
+ }
1494
+ else if ("control" in altResult) {
1495
+ catchControl = altResult.control;
1496
+ }
1497
+ else {
1498
+ catchFallbackRef = altResult.sourceRef;
1499
+ catchFallbackInternalWires = wires.splice(preLen);
1500
+ }
1501
+ }
1502
+ const lastAttrs = {
1503
+ ...(nullAltRefs.length > 0 ? { falsyFallbackRefs: nullAltRefs } : {}),
1504
+ ...(falsyFallback ? { falsyFallback } : {}),
1505
+ ...(falsyControl ? { falsyControl } : {}),
1506
+ ...(nullishFallback ? { nullishFallback } : {}),
1507
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
1508
+ ...(nullishControl ? { nullishControl } : {}),
1509
+ ...(catchFallback ? { catchFallback } : {}),
1510
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
1511
+ ...(catchControl ? { catchControl } : {}),
1512
+ };
1513
+ if (segs) {
1514
+ const concatOutRef = desugarTemplateStringFn(segs, elemLineNum, iterName);
1515
+ const elemToRefWithElement = { ...elemToRef, element: true };
1516
+ wires.push({
1517
+ from: concatOutRef,
1518
+ to: elemToRefWithElement,
1519
+ pipe: true,
1520
+ ...lastAttrs,
1521
+ });
1522
+ }
1523
+ else {
1524
+ wires.push({ value: raw, to: elemToRef, ...lastAttrs });
1525
+ }
1526
+ wires.push(...nullishFallbackInternalWires);
1527
+ wires.push(...catchFallbackInternalWires);
1528
+ continue;
1529
+ }
1530
+ const elemSourceNode = sub(elemLine, "elemSource");
1531
+ const elemFirstParenNode = sub(elemLine, "elemFirstParenExpr");
1532
+ // Check if iterator-relative source (only for non-paren sources)
1533
+ let elemHeadNode;
1534
+ let elemPipeSegs = [];
1535
+ let elemSrcRoot = "";
1536
+ let elemSrcSegs = [];
1537
+ let elemSafe = false;
1538
+ if (elemSourceNode) {
1539
+ elemHeadNode = sub(elemSourceNode, "head");
1540
+ elemPipeSegs = subs(elemSourceNode, "pipeSegment");
1541
+ const extracted = extractAddressPath(elemHeadNode);
1542
+ elemSrcRoot = extracted.root;
1543
+ elemSrcSegs = extracted.segments;
1544
+ elemSafe = !!extracted.rootSafe;
1545
+ }
1546
+ // ── Nested array mapping: .legs <- j.legs[] as l { ... } ──
1547
+ const nestedArrayNode = elemC.nestedArrayMapping?.[0];
1548
+ if (nestedArrayNode) {
1549
+ // Emit the pass-through wire for the inner array source
1550
+ let innerFromRef;
1551
+ if (elemSrcRoot === iterName && elemPipeSegs.length === 0) {
1552
+ innerFromRef = {
1553
+ module: SELF_MODULE,
1554
+ type: bridgeType,
1555
+ field: bridgeField,
1556
+ element: true,
1557
+ path: elemSrcSegs,
1558
+ };
1559
+ }
1560
+ else {
1561
+ innerFromRef = buildSourceExpr(elemSourceNode, elemLineNum);
1562
+ }
1563
+ const innerToRef = {
1564
+ module: SELF_MODULE,
1565
+ type: bridgeType,
1566
+ field: bridgeField,
1567
+ path: elemToPath,
1568
+ };
1569
+ wires.push({ from: innerFromRef, to: innerToRef });
1570
+ // Register the inner iterator
1571
+ const innerIterName = extractNameToken(sub(nestedArrayNode, "iterName"));
1572
+ assertNotReserved(innerIterName, elemLineNum, "iterator handle");
1573
+ // Key by the joined path for nested arrays (e.g. "legs" or "journeys.legs")
1574
+ const iterKey = elemToPath.join(".");
1575
+ arrayIterators[iterKey] = innerIterName;
1576
+ // Recurse into nested element lines
1577
+ const nestedWithDecls = subs(nestedArrayNode, "elementWithDecl");
1578
+ const nestedCleanup = processLocalBindings?.(nestedWithDecls, innerIterName);
1579
+ processElementLines(subs(nestedArrayNode, "elementLine"), elemToPath, innerIterName, bridgeType, bridgeField, wires, arrayIterators, buildSourceExpr, extractCoalesceAlt, desugarExprChain, extractTernaryBranchFn, processLocalBindings, desugarTemplateStringFn, desugarNotFn, resolveParenExprFn);
1580
+ nestedCleanup?.();
1581
+ continue;
1582
+ }
1583
+ // ── Element pull wire (expression or plain) ──
1584
+ const elemToRef = {
1585
+ module: SELF_MODULE,
1586
+ type: bridgeType,
1587
+ field: bridgeField,
1588
+ path: elemToPath,
1589
+ };
1590
+ const sourceParts = [];
1591
+ const elemExprOps = subs(elemLine, "elemExprOp");
1592
+ // Compute condition ref (expression chain result or plain source)
1593
+ let elemCondRef;
1594
+ let elemCondIsPipeFork;
1595
+ if (elemFirstParenNode && resolveParenExprFn) {
1596
+ // First source is a parenthesized sub-expression
1597
+ const parenRef = resolveParenExprFn(elemFirstParenNode, elemLineNum, iterName, elemSafe || undefined);
1598
+ if (elemExprOps.length > 0 && desugarExprChain) {
1599
+ const elemExprRights = subs(elemLine, "elemExprRight");
1600
+ elemCondRef = desugarExprChain(parenRef, elemExprOps, elemExprRights, elemLineNum, iterName, elemSafe || undefined);
1601
+ }
1602
+ else {
1603
+ elemCondRef = parenRef;
1604
+ }
1605
+ elemCondIsPipeFork = true;
1606
+ }
1607
+ else if (elemExprOps.length > 0 && desugarExprChain) {
1608
+ // Expression in element line — desugar then merge with fallback path
1609
+ const elemExprRights = subs(elemLine, "elemExprRight");
1610
+ let leftRef;
1611
+ if (elemSrcRoot === iterName && elemPipeSegs.length === 0) {
1612
+ leftRef = {
1613
+ module: SELF_MODULE,
1614
+ type: bridgeType,
1615
+ field: bridgeField,
1616
+ element: true,
1617
+ path: elemSrcSegs,
1618
+ };
1619
+ }
1620
+ else {
1621
+ leftRef = buildSourceExpr(elemSourceNode, elemLineNum);
1622
+ }
1623
+ elemCondRef = desugarExprChain(leftRef, elemExprOps, elemExprRights, elemLineNum, iterName, elemSafe || undefined);
1624
+ elemCondIsPipeFork = true;
1625
+ }
1626
+ else if (elemSrcRoot === iterName && elemPipeSegs.length === 0) {
1627
+ elemCondRef = {
1628
+ module: SELF_MODULE,
1629
+ type: bridgeType,
1630
+ field: bridgeField,
1631
+ element: true,
1632
+ path: elemSrcSegs,
1633
+ };
1634
+ elemCondIsPipeFork = false;
1635
+ }
1636
+ else {
1637
+ elemCondRef = buildSourceExpr(elemSourceNode, elemLineNum);
1638
+ elemCondIsPipeFork =
1639
+ elemCondRef.instance != null &&
1640
+ elemCondRef.path.length === 0 &&
1641
+ elemPipeSegs.length > 0;
1642
+ }
1643
+ // ── Apply `not` prefix if present (element context) ──
1644
+ if (elemC.elemNotPrefix?.[0] && desugarNotFn) {
1645
+ elemCondRef = desugarNotFn(elemCondRef, elemLineNum, elemSafe || undefined);
1646
+ elemCondIsPipeFork = true;
1647
+ }
1648
+ // ── Ternary wire in element context ──
1649
+ const elemTernaryOp = elemC.elemTernaryOp?.[0];
1650
+ if (elemTernaryOp && extractTernaryBranchFn) {
1651
+ const thenNode = sub(elemLine, "elemThenBranch");
1652
+ const elseNode = sub(elemLine, "elemElseBranch");
1653
+ const thenBranch = extractTernaryBranchFn(thenNode, elemLineNum, iterName);
1654
+ const elseBranch = extractTernaryBranchFn(elseNode, elemLineNum, iterName);
1655
+ // Process || null-coalesce alternatives.
1656
+ let elemFalsyFallback;
1657
+ let elemFalsyControl;
1658
+ const elemNullAltRefs = [];
1659
+ for (const alt of subs(elemLine, "elemNullAlt")) {
1660
+ const altResult = extractCoalesceAltIterAware(alt, elemLineNum);
1661
+ if ("literal" in altResult) {
1662
+ elemFalsyFallback = altResult.literal;
1663
+ }
1664
+ else if ("control" in altResult) {
1665
+ elemFalsyControl = altResult.control;
1666
+ }
1667
+ else {
1668
+ elemNullAltRefs.push(altResult.sourceRef);
1669
+ }
1670
+ }
1671
+ // Process ?? nullish fallback.
1672
+ let elemNullishFallback;
1673
+ let elemNullishControl;
1674
+ let elemNullishFallbackRef;
1675
+ let elemNullishFallbackInternalWires = [];
1676
+ const elemNullishAlt = sub(elemLine, "elemNullishAlt");
1677
+ if (elemNullishAlt) {
1678
+ const preLen = wires.length;
1679
+ const altResult = extractCoalesceAltIterAware(elemNullishAlt, elemLineNum);
1680
+ if ("literal" in altResult) {
1681
+ elemNullishFallback = altResult.literal;
1682
+ }
1683
+ else if ("control" in altResult) {
1684
+ elemNullishControl = altResult.control;
1685
+ }
1686
+ else {
1687
+ elemNullishFallbackRef = altResult.sourceRef;
1688
+ elemNullishFallbackInternalWires = wires.splice(preLen);
1689
+ }
1690
+ }
1691
+ // Process catch error fallback.
1692
+ let elemCatchFallback;
1693
+ let elemCatchControl;
1694
+ let elemCatchFallbackRef;
1695
+ let elemCatchFallbackInternalWires = [];
1696
+ const elemCatchAlt = sub(elemLine, "elemCatchAlt");
1697
+ if (elemCatchAlt) {
1698
+ const preLen = wires.length;
1699
+ const altResult = extractCoalesceAltIterAware(elemCatchAlt, elemLineNum);
1700
+ if ("literal" in altResult) {
1701
+ elemCatchFallback = altResult.literal;
1702
+ }
1703
+ else if ("control" in altResult) {
1704
+ elemCatchControl = altResult.control;
1705
+ }
1706
+ else {
1707
+ elemCatchFallbackRef = altResult.sourceRef;
1708
+ elemCatchFallbackInternalWires = wires.splice(preLen);
1709
+ }
1710
+ }
1711
+ wires.push({
1712
+ cond: elemCondRef,
1713
+ ...(thenBranch.kind === "ref"
1714
+ ? { thenRef: thenBranch.ref }
1715
+ : { thenValue: thenBranch.value }),
1716
+ ...(elseBranch.kind === "ref"
1717
+ ? { elseRef: elseBranch.ref }
1718
+ : { elseValue: elseBranch.value }),
1719
+ ...(elemNullAltRefs.length > 0
1720
+ ? { falsyFallbackRefs: elemNullAltRefs }
1721
+ : {}),
1722
+ ...(elemFalsyFallback !== undefined
1723
+ ? { falsyFallback: elemFalsyFallback }
1724
+ : {}),
1725
+ ...(elemFalsyControl ? { falsyControl: elemFalsyControl } : {}),
1726
+ ...(elemNullishFallback !== undefined
1727
+ ? { nullishFallback: elemNullishFallback }
1728
+ : {}),
1729
+ ...(elemNullishFallbackRef !== undefined
1730
+ ? { nullishFallbackRef: elemNullishFallbackRef }
1731
+ : {}),
1732
+ ...(elemNullishControl ? { nullishControl: elemNullishControl } : {}),
1733
+ ...(elemCatchFallback !== undefined
1734
+ ? { catchFallback: elemCatchFallback }
1735
+ : {}),
1736
+ ...(elemCatchFallbackRef !== undefined
1737
+ ? { catchFallbackRef: elemCatchFallbackRef }
1738
+ : {}),
1739
+ ...(elemCatchControl ? { catchControl: elemCatchControl } : {}),
1740
+ to: elemToRef,
1741
+ });
1742
+ wires.push(...elemNullishFallbackInternalWires);
1743
+ wires.push(...elemCatchFallbackInternalWires);
1744
+ continue;
1745
+ }
1746
+ sourceParts.push({ ref: elemCondRef, isPipeFork: elemCondIsPipeFork });
1747
+ // || alternatives
1748
+ let falsyFallback;
1749
+ let falsyControl;
1750
+ for (const alt of subs(elemLine, "elemNullAlt")) {
1751
+ const altResult = extractCoalesceAltIterAware(alt, elemLineNum);
1752
+ if ("literal" in altResult) {
1753
+ falsyFallback = altResult.literal;
1754
+ }
1755
+ else if ("control" in altResult) {
1756
+ falsyControl = altResult.control;
1757
+ }
1758
+ else {
1759
+ sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false });
1760
+ }
1761
+ }
1762
+ // ?? nullish fallback
1763
+ let nullishFallback;
1764
+ let nullishControl;
1765
+ let nullishFallbackRef;
1766
+ let nullishFallbackInternalWires = [];
1767
+ const nullishAlt = sub(elemLine, "elemNullishAlt");
1768
+ if (nullishAlt) {
1769
+ const preLen = wires.length;
1770
+ const altResult = extractCoalesceAltIterAware(nullishAlt, elemLineNum);
1771
+ if ("literal" in altResult) {
1772
+ nullishFallback = altResult.literal;
1773
+ }
1774
+ else if ("control" in altResult) {
1775
+ nullishControl = altResult.control;
1776
+ }
1777
+ else {
1778
+ nullishFallbackRef = altResult.sourceRef;
1779
+ nullishFallbackInternalWires = wires.splice(preLen);
1780
+ }
1781
+ }
1782
+ // catch error fallback
1783
+ let catchFallback;
1784
+ let catchControl;
1785
+ let catchFallbackRef;
1786
+ let catchFallbackInternalWires = [];
1787
+ const catchAlt = sub(elemLine, "elemCatchAlt");
1788
+ if (catchAlt) {
1789
+ const preLen = wires.length;
1790
+ const altResult = extractCoalesceAltIterAware(catchAlt, elemLineNum);
1791
+ if ("literal" in altResult) {
1792
+ catchFallback = altResult.literal;
1793
+ }
1794
+ else if ("control" in altResult) {
1795
+ catchControl = altResult.control;
1796
+ }
1797
+ else {
1798
+ catchFallbackRef = altResult.sourceRef;
1799
+ catchFallbackInternalWires = wires.splice(preLen);
1800
+ }
1801
+ }
1802
+ // Emit wire
1803
+ const { ref: fromRef, isPipeFork } = sourceParts[0];
1804
+ const fallbackRefs = sourceParts.length > 1
1805
+ ? sourceParts.slice(1).map((p) => p.ref)
1806
+ : undefined;
1807
+ const wireAttrs = {
1808
+ ...(isPipeFork ? { pipe: true } : {}),
1809
+ ...(fallbackRefs ? { falsyFallbackRefs: fallbackRefs } : {}),
1810
+ ...(falsyFallback ? { falsyFallback } : {}),
1811
+ ...(falsyControl ? { falsyControl } : {}),
1812
+ ...(nullishFallback ? { nullishFallback } : {}),
1813
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
1814
+ ...(nullishControl ? { nullishControl } : {}),
1815
+ ...(catchFallback ? { catchFallback } : {}),
1816
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
1817
+ ...(catchControl ? { catchControl } : {}),
1818
+ };
1819
+ wires.push({ from: fromRef, to: elemToRef, ...wireAttrs });
1820
+ wires.push(...nullishFallbackInternalWires);
1821
+ wires.push(...catchFallbackInternalWires);
1822
+ }
1823
+ else if (elemC.elemScopeBlock) {
1824
+ // ── Path scope block inside array mapping: .field { .sub <- ... } ──
1825
+ const scopeLines = subs(elemLine, "elemScopeLine");
1826
+ processElementScopeLines(scopeLines, elemToPath, [], iterName, bridgeType, bridgeField, wires, buildSourceExpr, extractCoalesceAlt, desugarExprChain, extractTernaryBranchFn, desugarTemplateStringFn, desugarNotFn, resolveParenExprFn);
1827
+ }
1828
+ }
1829
+ }
1830
+ // ─────────────────────────────────────────────────────────────────────────────
1831
+ /**
1832
+ * Recursively flatten path-scope blocks (`pathScopeLine` CST nodes) that
1833
+ * appear inside an array-mapping block. Mirrors `processScopeLines` in
1834
+ * `buildBridgeBody` but emits element-context wires (same as
1835
+ * `processElementLines`).
1836
+ */
1837
+ function processElementScopeLines(scopeLines, arrayToPath, pathPrefix, iterName, bridgeType, bridgeField, wires, buildSourceExpr, extractCoalesceAlt, desugarExprChain, extractTernaryBranchFn, desugarTemplateStringFn, desugarNotFn, resolveParenExprFn) {
1838
+ function extractCoalesceAltIterAware(altNode, lineNum) {
1839
+ const c = altNode.children;
1840
+ if (c.sourceAlt) {
1841
+ const srcNode = c.sourceAlt[0];
1842
+ const headNode = sub(srcNode, "head");
1843
+ if (headNode) {
1844
+ const { root, segments } = extractAddressPath(headNode);
1845
+ const pipeSegs = subs(srcNode, "pipeSegment");
1846
+ if (root === iterName && pipeSegs.length === 0) {
1847
+ return {
1848
+ sourceRef: {
1849
+ module: SELF_MODULE,
1850
+ type: bridgeType,
1851
+ field: bridgeField,
1852
+ element: true,
1853
+ path: segments,
1854
+ },
1855
+ };
1856
+ }
1857
+ }
1858
+ }
1859
+ return extractCoalesceAlt(altNode, lineNum, iterName);
1860
+ }
1861
+ for (const scopeLine of scopeLines) {
1862
+ const sc = scopeLine.children;
1863
+ const scopeLineNum = line(findFirstToken(scopeLine));
1864
+ const targetStr = extractDottedPathStr(sub(scopeLine, "scopeTarget"));
1865
+ const scopeSegs = parsePath(targetStr);
1866
+ const fullSegs = [...pathPrefix, ...scopeSegs];
1867
+ // ── Nested scope: .field { ... } ──
1868
+ const nestedScopeLines = subs(scopeLine, "pathScopeLine");
1869
+ if (nestedScopeLines.length > 0 && !sc.scopeEquals && !sc.scopeArrow) {
1870
+ processElementScopeLines(nestedScopeLines, arrayToPath, fullSegs, iterName, bridgeType, bridgeField, wires, buildSourceExpr, extractCoalesceAlt, desugarExprChain, extractTernaryBranchFn, desugarTemplateStringFn, desugarNotFn, resolveParenExprFn);
1871
+ continue;
1872
+ }
1873
+ const elemToPath = [...arrayToPath, ...fullSegs];
1874
+ // ── Constant wire: .field = value ──
1875
+ if (sc.scopeEquals) {
1876
+ const value = extractBareValue(sub(scopeLine, "scopeValue"));
1877
+ wires.push({
1878
+ value,
1879
+ to: {
1880
+ module: SELF_MODULE,
1881
+ type: bridgeType,
1882
+ field: bridgeField,
1883
+ element: true,
1884
+ path: elemToPath,
1885
+ },
1886
+ });
1887
+ continue;
1888
+ }
1889
+ // ── Pull wire: .field <- source [modifiers] ──
1890
+ if (sc.scopeArrow) {
1891
+ const elemToRef = {
1892
+ module: SELF_MODULE,
1893
+ type: bridgeType,
1894
+ field: bridgeField,
1895
+ path: elemToPath,
1896
+ };
1897
+ // String source (template or plain): .field <- "..."
1898
+ const stringSourceToken = sc.scopeStringSource?.[0];
1899
+ if (stringSourceToken && desugarTemplateStringFn) {
1900
+ const raw = stringSourceToken.image.slice(1, -1);
1901
+ const segs = parseTemplateString(raw);
1902
+ let falsyFallback;
1903
+ let falsyControl;
1904
+ const nullAltRefs = [];
1905
+ for (const alt of subs(scopeLine, "scopeNullAlt")) {
1906
+ const altResult = extractCoalesceAltIterAware(alt, scopeLineNum);
1907
+ if ("literal" in altResult)
1908
+ falsyFallback = altResult.literal;
1909
+ else if ("control" in altResult)
1910
+ falsyControl = altResult.control;
1911
+ else
1912
+ nullAltRefs.push(altResult.sourceRef);
1913
+ }
1914
+ let nullishFallback;
1915
+ let nullishControl;
1916
+ let nullishFallbackRef;
1917
+ let nullishFallbackInternalWires = [];
1918
+ const nullishAlt = sub(scopeLine, "scopeNullishAlt");
1919
+ if (nullishAlt) {
1920
+ const preLen = wires.length;
1921
+ const altResult = extractCoalesceAltIterAware(nullishAlt, scopeLineNum);
1922
+ if ("literal" in altResult)
1923
+ nullishFallback = altResult.literal;
1924
+ else if ("control" in altResult)
1925
+ nullishControl = altResult.control;
1926
+ else {
1927
+ nullishFallbackRef = altResult.sourceRef;
1928
+ nullishFallbackInternalWires = wires.splice(preLen);
1929
+ }
1930
+ }
1931
+ let catchFallback;
1932
+ let catchControl;
1933
+ let catchFallbackRef;
1934
+ let catchFallbackInternalWires = [];
1935
+ const catchAlt = sub(scopeLine, "scopeCatchAlt");
1936
+ if (catchAlt) {
1937
+ const preLen = wires.length;
1938
+ const altResult = extractCoalesceAltIterAware(catchAlt, scopeLineNum);
1939
+ if ("literal" in altResult)
1940
+ catchFallback = altResult.literal;
1941
+ else if ("control" in altResult)
1942
+ catchControl = altResult.control;
1943
+ else {
1944
+ catchFallbackRef = altResult.sourceRef;
1945
+ catchFallbackInternalWires = wires.splice(preLen);
1946
+ }
1947
+ }
1948
+ const lastAttrs = {
1949
+ ...(nullAltRefs.length > 0 ? { falsyFallbackRefs: nullAltRefs } : {}),
1950
+ ...(falsyFallback ? { falsyFallback } : {}),
1951
+ ...(falsyControl ? { falsyControl } : {}),
1952
+ ...(nullishFallback ? { nullishFallback } : {}),
1953
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
1954
+ ...(nullishControl ? { nullishControl } : {}),
1955
+ ...(catchFallback ? { catchFallback } : {}),
1956
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
1957
+ ...(catchControl ? { catchControl } : {}),
1958
+ };
1959
+ if (segs) {
1960
+ const concatOutRef = desugarTemplateStringFn(segs, scopeLineNum, iterName);
1961
+ wires.push({
1962
+ from: concatOutRef,
1963
+ to: { ...elemToRef, element: true },
1964
+ pipe: true,
1965
+ ...lastAttrs,
1966
+ });
1967
+ }
1968
+ else {
1969
+ wires.push({ value: raw, to: elemToRef, ...lastAttrs });
1970
+ }
1971
+ wires.push(...nullishFallbackInternalWires);
1972
+ wires.push(...catchFallbackInternalWires);
1973
+ continue;
1974
+ }
1975
+ // Normal source expression
1976
+ const scopeSourceNode = sub(scopeLine, "scopeSource");
1977
+ const scopeFirstParenNode = sub(scopeLine, "scopeFirstParenExpr");
1978
+ let scopeHeadNode;
1979
+ let scopePipeSegs = [];
1980
+ let srcRoot = "";
1981
+ let srcSegs = [];
1982
+ let scopeSafe = false;
1983
+ if (scopeSourceNode) {
1984
+ scopeHeadNode = sub(scopeSourceNode, "head");
1985
+ scopePipeSegs = subs(scopeSourceNode, "pipeSegment");
1986
+ const extracted = extractAddressPath(scopeHeadNode);
1987
+ srcRoot = extracted.root;
1988
+ srcSegs = extracted.segments;
1989
+ scopeSafe = !!extracted.rootSafe;
1990
+ }
1991
+ const exprOps = subs(scopeLine, "scopeExprOp");
1992
+ let condRef;
1993
+ let condIsPipeFork;
1994
+ if (scopeFirstParenNode && resolveParenExprFn) {
1995
+ const parenRef = resolveParenExprFn(scopeFirstParenNode, scopeLineNum, iterName, scopeSafe || undefined);
1996
+ if (exprOps.length > 0 && desugarExprChain) {
1997
+ const exprRights = subs(scopeLine, "scopeExprRight");
1998
+ condRef = desugarExprChain(parenRef, exprOps, exprRights, scopeLineNum, iterName, scopeSafe || undefined);
1999
+ }
2000
+ else {
2001
+ condRef = parenRef;
2002
+ }
2003
+ condIsPipeFork = true;
2004
+ }
2005
+ else if (exprOps.length > 0 && desugarExprChain) {
2006
+ const exprRights = subs(scopeLine, "scopeExprRight");
2007
+ let leftRef;
2008
+ if (srcRoot === iterName && scopePipeSegs.length === 0) {
2009
+ leftRef = {
2010
+ module: SELF_MODULE,
2011
+ type: bridgeType,
2012
+ field: bridgeField,
2013
+ element: true,
2014
+ path: srcSegs,
2015
+ };
2016
+ }
2017
+ else {
2018
+ leftRef = buildSourceExpr(scopeSourceNode, scopeLineNum);
2019
+ }
2020
+ condRef = desugarExprChain(leftRef, exprOps, exprRights, scopeLineNum, iterName, scopeSafe || undefined);
2021
+ condIsPipeFork = true;
2022
+ }
2023
+ else if (srcRoot === iterName && scopePipeSegs.length === 0) {
2024
+ condRef = {
2025
+ module: SELF_MODULE,
2026
+ type: bridgeType,
2027
+ field: bridgeField,
2028
+ element: true,
2029
+ path: srcSegs,
2030
+ };
2031
+ condIsPipeFork = false;
2032
+ }
2033
+ else {
2034
+ condRef = buildSourceExpr(scopeSourceNode, scopeLineNum);
2035
+ condIsPipeFork =
2036
+ condRef.instance != null &&
2037
+ condRef.path.length === 0 &&
2038
+ scopePipeSegs.length > 0;
2039
+ }
2040
+ // ── Apply `not` prefix if present (scope context) ──
2041
+ if (sc.scopeNotPrefix?.[0] && desugarNotFn) {
2042
+ condRef = desugarNotFn(condRef, scopeLineNum, scopeSafe || undefined);
2043
+ condIsPipeFork = true;
2044
+ }
2045
+ // Ternary wire: .field <- cond ? then : else
2046
+ const scopeTernaryOp = sc.scopeTernaryOp?.[0];
2047
+ if (scopeTernaryOp && extractTernaryBranchFn) {
2048
+ const thenNode = sub(scopeLine, "scopeThenBranch");
2049
+ const elseNode = sub(scopeLine, "scopeElseBranch");
2050
+ const thenBranch = extractTernaryBranchFn(thenNode, scopeLineNum, iterName);
2051
+ const elseBranch = extractTernaryBranchFn(elseNode, scopeLineNum, iterName);
2052
+ let falsyFallback;
2053
+ let falsyControl;
2054
+ const nullAltRefs = [];
2055
+ for (const alt of subs(scopeLine, "scopeNullAlt")) {
2056
+ const altResult = extractCoalesceAltIterAware(alt, scopeLineNum);
2057
+ if ("literal" in altResult)
2058
+ falsyFallback = altResult.literal;
2059
+ else if ("control" in altResult)
2060
+ falsyControl = altResult.control;
2061
+ else
2062
+ nullAltRefs.push(altResult.sourceRef);
2063
+ }
2064
+ let nullishFallback;
2065
+ let nullishControl;
2066
+ let nullishFallbackRef;
2067
+ let nullishFallbackInternalWires = [];
2068
+ const nullishAlt = sub(scopeLine, "scopeNullishAlt");
2069
+ if (nullishAlt) {
2070
+ const preLen = wires.length;
2071
+ const altResult = extractCoalesceAltIterAware(nullishAlt, scopeLineNum);
2072
+ if ("literal" in altResult)
2073
+ nullishFallback = altResult.literal;
2074
+ else if ("control" in altResult)
2075
+ nullishControl = altResult.control;
2076
+ else {
2077
+ nullishFallbackRef = altResult.sourceRef;
2078
+ nullishFallbackInternalWires = wires.splice(preLen);
2079
+ }
2080
+ }
2081
+ let catchFallback;
2082
+ let catchControl;
2083
+ let catchFallbackRef;
2084
+ let catchFallbackInternalWires = [];
2085
+ const catchAlt = sub(scopeLine, "scopeCatchAlt");
2086
+ if (catchAlt) {
2087
+ const preLen = wires.length;
2088
+ const altResult = extractCoalesceAltIterAware(catchAlt, scopeLineNum);
2089
+ if ("literal" in altResult)
2090
+ catchFallback = altResult.literal;
2091
+ else if ("control" in altResult)
2092
+ catchControl = altResult.control;
2093
+ else {
2094
+ catchFallbackRef = altResult.sourceRef;
2095
+ catchFallbackInternalWires = wires.splice(preLen);
2096
+ }
2097
+ }
2098
+ wires.push({
2099
+ cond: condRef,
2100
+ ...(thenBranch.kind === "ref"
2101
+ ? { thenRef: thenBranch.ref }
2102
+ : { thenValue: thenBranch.value }),
2103
+ ...(elseBranch.kind === "ref"
2104
+ ? { elseRef: elseBranch.ref }
2105
+ : { elseValue: elseBranch.value }),
2106
+ ...(nullAltRefs.length > 0 ? { falsyFallbackRefs: nullAltRefs } : {}),
2107
+ ...(falsyFallback !== undefined ? { falsyFallback } : {}),
2108
+ ...(falsyControl ? { falsyControl } : {}),
2109
+ ...(nullishFallback !== undefined ? { nullishFallback } : {}),
2110
+ ...(nullishFallbackRef !== undefined ? { nullishFallbackRef } : {}),
2111
+ ...(nullishControl ? { nullishControl } : {}),
2112
+ ...(catchFallback !== undefined ? { catchFallback } : {}),
2113
+ ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}),
2114
+ ...(catchControl ? { catchControl } : {}),
2115
+ to: elemToRef,
2116
+ });
2117
+ wires.push(...nullishFallbackInternalWires);
2118
+ wires.push(...catchFallbackInternalWires);
2119
+ continue;
2120
+ }
2121
+ const sourceParts = [];
2122
+ sourceParts.push({ ref: condRef, isPipeFork: condIsPipeFork });
2123
+ let falsyFallback;
2124
+ let falsyControl;
2125
+ for (const alt of subs(scopeLine, "scopeNullAlt")) {
2126
+ const altResult = extractCoalesceAltIterAware(alt, scopeLineNum);
2127
+ if ("literal" in altResult)
2128
+ falsyFallback = altResult.literal;
2129
+ else if ("control" in altResult)
2130
+ falsyControl = altResult.control;
2131
+ else
2132
+ sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false });
2133
+ }
2134
+ let nullishFallback;
2135
+ let nullishControl;
2136
+ let nullishFallbackRef;
2137
+ let nullishFallbackInternalWires = [];
2138
+ const nullishAlt = sub(scopeLine, "scopeNullishAlt");
2139
+ if (nullishAlt) {
2140
+ const preLen = wires.length;
2141
+ const altResult = extractCoalesceAltIterAware(nullishAlt, scopeLineNum);
2142
+ if ("literal" in altResult)
2143
+ nullishFallback = altResult.literal;
2144
+ else if ("control" in altResult)
2145
+ nullishControl = altResult.control;
2146
+ else {
2147
+ nullishFallbackRef = altResult.sourceRef;
2148
+ nullishFallbackInternalWires = wires.splice(preLen);
2149
+ }
2150
+ }
2151
+ let catchFallback;
2152
+ let catchControl;
2153
+ let catchFallbackRef;
2154
+ let catchFallbackInternalWires = [];
2155
+ const catchAlt = sub(scopeLine, "scopeCatchAlt");
2156
+ if (catchAlt) {
2157
+ const preLen = wires.length;
2158
+ const altResult = extractCoalesceAltIterAware(catchAlt, scopeLineNum);
2159
+ if ("literal" in altResult)
2160
+ catchFallback = altResult.literal;
2161
+ else if ("control" in altResult)
2162
+ catchControl = altResult.control;
2163
+ else {
2164
+ catchFallbackRef = altResult.sourceRef;
2165
+ catchFallbackInternalWires = wires.splice(preLen);
2166
+ }
2167
+ }
2168
+ const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0];
2169
+ const fallbackRefs = sourceParts.length > 1
2170
+ ? sourceParts.slice(1).map((p) => p.ref)
2171
+ : undefined;
2172
+ const wireAttrs = {
2173
+ ...(isPipe ? { pipe: true } : {}),
2174
+ ...(fallbackRefs ? { falsyFallbackRefs: fallbackRefs } : {}),
2175
+ ...(falsyFallback ? { falsyFallback } : {}),
2176
+ ...(falsyControl ? { falsyControl } : {}),
2177
+ ...(nullishFallback ? { nullishFallback } : {}),
2178
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
2179
+ ...(nullishControl ? { nullishControl } : {}),
2180
+ ...(catchFallback ? { catchFallback } : {}),
2181
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
2182
+ ...(catchControl ? { catchControl } : {}),
2183
+ };
2184
+ wires.push({ from: fromRef, to: elemToRef, ...wireAttrs });
2185
+ wires.push(...nullishFallbackInternalWires);
2186
+ wires.push(...catchFallbackInternalWires);
2187
+ }
2188
+ }
2189
+ }
2190
+ // ═══════════════════════════════════════════════════════════════════════════
2191
+ // Main AST builder
2192
+ // ═══════════════════════════════════════════════════════════════════════════
2193
+ function toBridgeAst(cst, previousInstructions) {
2194
+ const instructions = [];
2195
+ const startLines = new Map();
2196
+ // If called from passthrough expansion, seed with prior context
2197
+ const contextInstructions = previousInstructions
2198
+ ? [...previousInstructions]
2199
+ : [];
2200
+ // ── Version check ──
2201
+ const versionDecl = sub(cst, "versionDecl");
2202
+ if (!versionDecl) {
2203
+ throw new Error(`Missing version declaration. Bridge files must begin with: version ${BRIDGE_VERSION}`);
2204
+ }
2205
+ const versionTok = tok(versionDecl, "ver");
2206
+ const versionNum = versionTok?.image;
2207
+ if (versionNum !== BRIDGE_VERSION) {
2208
+ throw new Error(`Unsupported bridge version "${versionNum}". This parser requires: version ${BRIDGE_VERSION}`);
2209
+ }
2210
+ const tagged = [];
2211
+ for (const n of subs(cst, "constDecl"))
2212
+ tagged.push({
2213
+ offset: findFirstToken(n)?.startOffset ?? 0,
2214
+ kind: "const",
2215
+ node: n,
2216
+ });
2217
+ for (const n of subs(cst, "toolBlock"))
2218
+ tagged.push({
2219
+ offset: findFirstToken(n)?.startOffset ?? 0,
2220
+ kind: "tool",
2221
+ node: n,
2222
+ });
2223
+ for (const n of subs(cst, "defineBlock"))
2224
+ tagged.push({
2225
+ offset: findFirstToken(n)?.startOffset ?? 0,
2226
+ kind: "define",
2227
+ node: n,
2228
+ });
2229
+ for (const n of subs(cst, "bridgeBlock"))
2230
+ tagged.push({
2231
+ offset: findFirstToken(n)?.startOffset ?? 0,
2232
+ kind: "bridge",
2233
+ node: n,
2234
+ });
2235
+ tagged.sort((a, b) => a.offset - b.offset);
2236
+ for (const item of tagged) {
2237
+ const startLine = findFirstToken(item.node)?.startLine ?? 1;
2238
+ switch (item.kind) {
2239
+ case "const": {
2240
+ const inst = buildConstDef(item.node);
2241
+ instructions.push(inst);
2242
+ startLines.set(inst, startLine);
2243
+ break;
2244
+ }
2245
+ case "tool": {
2246
+ const inst = buildToolDef(item.node, [
2247
+ ...contextInstructions,
2248
+ ...instructions,
2249
+ ]);
2250
+ instructions.push(inst);
2251
+ startLines.set(inst, startLine);
2252
+ break;
2253
+ }
2254
+ case "define": {
2255
+ const inst = buildDefineDef(item.node);
2256
+ instructions.push(inst);
2257
+ startLines.set(inst, startLine);
2258
+ break;
2259
+ }
2260
+ case "bridge": {
2261
+ const newInsts = buildBridge(item.node, [
2262
+ ...contextInstructions,
2263
+ ...instructions,
2264
+ ]);
2265
+ for (const bi of newInsts) {
2266
+ instructions.push(bi);
2267
+ startLines.set(bi, startLine);
2268
+ }
2269
+ break;
2270
+ }
2271
+ }
2272
+ }
2273
+ return { instructions, startLines };
2274
+ }
2275
+ // ── Const ───────────────────────────────────────────────────────────────
2276
+ function buildConstDef(node) {
2277
+ const nameNode = sub(node, "constName");
2278
+ const name = extractNameToken(nameNode);
2279
+ const lineNum = line(findFirstToken(nameNode));
2280
+ assertNotReserved(name, lineNum, "const name");
2281
+ const valueNode = sub(node, "constValue");
2282
+ const raw = extractJsonValue(valueNode);
2283
+ // Validate JSON
2284
+ try {
2285
+ JSON.parse(raw);
2286
+ }
2287
+ catch {
2288
+ throw new Error(`Line ${lineNum}: Invalid JSON value for const "${name}": ${raw}`);
2289
+ }
2290
+ return { kind: "const", name, value: raw };
2291
+ }
2292
+ // ── Tool ────────────────────────────────────────────────────────────────
2293
+ function buildToolDef(node, previousInstructions) {
2294
+ const toolName = extractDottedName(sub(node, "toolName"));
2295
+ const source = extractDottedName(sub(node, "toolSource"));
2296
+ const lineNum = line(findFirstToken(sub(node, "toolName")));
2297
+ assertNotReserved(toolName, lineNum, "tool name");
2298
+ const isKnownTool = previousInstructions.some((inst) => inst.kind === "tool" && inst.name === source);
2299
+ const deps = [];
2300
+ const wires = [];
2301
+ for (const bodyLine of subs(node, "toolBodyLine")) {
2302
+ const c = bodyLine.children;
2303
+ // toolWithDecl
2304
+ const withNode = c.toolWithDecl?.[0];
2305
+ if (withNode) {
2306
+ const wc = withNode.children;
2307
+ if (wc.contextKw) {
2308
+ const alias = wc.alias
2309
+ ? extractNameToken(wc.alias[0])
2310
+ : "context";
2311
+ deps.push({ kind: "context", handle: alias });
2312
+ }
2313
+ else if (wc.constKw) {
2314
+ const alias = wc.constAlias
2315
+ ? extractNameToken(wc.constAlias[0])
2316
+ : "const";
2317
+ deps.push({ kind: "const", handle: alias });
2318
+ }
2319
+ else if (wc.toolName) {
2320
+ const tName = extractDottedName(wc.toolName[0]);
2321
+ const tAlias = extractNameToken(wc.toolAlias[0]);
2322
+ deps.push({ kind: "tool", handle: tAlias, tool: tName });
2323
+ }
2324
+ continue;
2325
+ }
2326
+ // toolOnError
2327
+ const onError = c.toolOnError?.[0];
2328
+ if (onError) {
2329
+ const oc = onError.children;
2330
+ if (oc.equalsOp) {
2331
+ const value = extractJsonValue(sub(onError, "errorValue"));
2332
+ wires.push({ kind: "onError", value });
2333
+ }
2334
+ else if (oc.arrowOp) {
2335
+ const source = extractDottedName(sub(onError, "errorSource"));
2336
+ wires.push({ kind: "onError", source });
2337
+ }
2338
+ continue;
2339
+ }
2340
+ // toolWire (merged constant + pull)
2341
+ const wireNode = c.toolWire?.[0];
2342
+ if (wireNode) {
2343
+ const wc = wireNode.children;
2344
+ const target = extractDottedPathStr(sub(wireNode, "target"));
2345
+ if (wc.equalsOp) {
2346
+ const value = extractBareValue(sub(wireNode, "value"));
2347
+ wires.push({ target, kind: "constant", value });
2348
+ }
2349
+ else if (wc.arrowOp) {
2350
+ const source = extractDottedName(sub(wireNode, "source"));
2351
+ wires.push({ target, kind: "pull", source });
2352
+ }
2353
+ continue;
2354
+ }
2355
+ }
2356
+ return {
2357
+ kind: "tool",
2358
+ name: toolName,
2359
+ fn: isKnownTool ? undefined : source,
2360
+ extends: isKnownTool ? source : undefined,
2361
+ deps,
2362
+ wires,
2363
+ };
2364
+ }
2365
+ // ── Define ──────────────────────────────────────────────────────────────
2366
+ function buildDefineDef(node) {
2367
+ const name = extractNameToken(sub(node, "defineName"));
2368
+ const lineNum = line(findFirstToken(sub(node, "defineName")));
2369
+ assertNotReserved(name, lineNum, "define name");
2370
+ const bodyLines = subs(node, "bridgeBodyLine");
2371
+ const { handles, wires, arrayIterators, pipeHandles, forces } = buildBridgeBody(bodyLines, "Define", name, [], lineNum);
2372
+ return {
2373
+ kind: "define",
2374
+ name,
2375
+ handles,
2376
+ wires,
2377
+ ...(Object.keys(arrayIterators).length > 0 ? { arrayIterators } : {}),
2378
+ ...(pipeHandles.length > 0 ? { pipeHandles } : {}),
2379
+ ...(forces.length > 0 ? { forces } : {}),
2380
+ };
2381
+ }
2382
+ // ── Bridge ──────────────────────────────────────────────────────────────
2383
+ function buildBridge(node, previousInstructions) {
2384
+ const typeName = extractNameToken(sub(node, "typeName"));
2385
+ const fieldName = extractNameToken(sub(node, "fieldName"));
2386
+ // Passthrough shorthand
2387
+ if (node.children.passthroughWith) {
2388
+ const passthroughName = extractDottedName(sub(node, "passthroughName"));
2389
+ const sHandle = passthroughName.includes(".")
2390
+ ? passthroughName.substring(passthroughName.lastIndexOf(".") + 1)
2391
+ : passthroughName;
2392
+ const expandedText = [
2393
+ `version ${BRIDGE_VERSION}`,
2394
+ `bridge ${typeName}.${fieldName} {`,
2395
+ ` with ${passthroughName} as ${sHandle}`,
2396
+ ` with input`,
2397
+ ` with output as __out`,
2398
+ ` ${sHandle} <- input`,
2399
+ ` __out <- ${sHandle}`,
2400
+ `}`,
2401
+ ].join("\n");
2402
+ const result = internalParse(expandedText, previousInstructions);
2403
+ const bridgeInst = result.find((i) => i.kind === "bridge");
2404
+ if (bridgeInst)
2405
+ bridgeInst.passthrough = passthroughName;
2406
+ return result;
2407
+ }
2408
+ // Full bridge block
2409
+ const bodyLines = subs(node, "bridgeBodyLine");
2410
+ const { handles, wires, arrayIterators, pipeHandles, forces } = buildBridgeBody(bodyLines, typeName, fieldName, previousInstructions, 0);
2411
+ // Inline define invocations
2412
+ const instanceCounters = new Map();
2413
+ for (const hb of handles) {
2414
+ if (hb.kind !== "tool")
2415
+ continue;
2416
+ const name = hb.name;
2417
+ const lastDot = name.lastIndexOf(".");
2418
+ if (lastDot !== -1) {
2419
+ const key = `${name.substring(0, lastDot)}:${name.substring(lastDot + 1)}`;
2420
+ instanceCounters.set(key, (instanceCounters.get(key) ?? 0) + 1);
2421
+ }
2422
+ else {
2423
+ const key = `Tools:${name}`;
2424
+ instanceCounters.set(key, (instanceCounters.get(key) ?? 0) + 1);
2425
+ }
2426
+ }
2427
+ const nextForkSeqRef = {
2428
+ value: pipeHandles.length > 0
2429
+ ? Math.max(...pipeHandles
2430
+ .map((p) => {
2431
+ const parts = p.key.split(":");
2432
+ return parseInt(parts[parts.length - 1]) || 0;
2433
+ })
2434
+ .filter((n) => n >= 100000)
2435
+ .map((n) => n - 100000 + 1), 0)
2436
+ : 0,
2437
+ };
2438
+ for (const hb of handles) {
2439
+ if (hb.kind !== "define")
2440
+ continue;
2441
+ const def = previousInstructions.find((inst) => inst.kind === "define" && inst.name === hb.name);
2442
+ if (!def) {
2443
+ throw new Error(`Define "${hb.name}" referenced by handle "${hb.handle}" not found`);
2444
+ }
2445
+ inlineDefine(hb.handle, def, typeName, fieldName, wires, pipeHandles, handles, instanceCounters, nextForkSeqRef);
2446
+ }
2447
+ const instructions = [];
2448
+ instructions.push({
2449
+ kind: "bridge",
2450
+ type: typeName,
2451
+ field: fieldName,
2452
+ handles,
2453
+ wires,
2454
+ arrayIterators: Object.keys(arrayIterators).length > 0 ? arrayIterators : undefined,
2455
+ pipeHandles: pipeHandles.length > 0 ? pipeHandles : undefined,
2456
+ forces: forces.length > 0 ? forces : undefined,
2457
+ });
2458
+ return instructions;
2459
+ }
2460
+ // ═══════════════════════════════════════════════════════════════════════════
2461
+ // Bridge/Define body builder
2462
+ // ═══════════════════════════════════════════════════════════════════════════
2463
+ function buildBridgeBody(bodyLines, bridgeType, bridgeField, previousInstructions, _lineOffset) {
2464
+ const handleRes = new Map();
2465
+ const handleBindings = [];
2466
+ const instanceCounters = new Map();
2467
+ const wires = [];
2468
+ const arrayIterators = {};
2469
+ let nextForkSeq = 0;
2470
+ const pipeHandleEntries = [];
2471
+ // ── Step 1: Process with-declarations ─────────────────────────────────
2472
+ for (const bodyLine of bodyLines) {
2473
+ const withNode = bodyLine.children.bridgeWithDecl?.[0];
2474
+ if (!withNode)
2475
+ continue;
2476
+ const wc = withNode.children;
2477
+ const lineNum = line(findFirstToken(withNode));
2478
+ const checkDuplicate = (handle) => {
2479
+ if (handleRes.has(handle)) {
2480
+ throw new Error(`Line ${lineNum}: Duplicate handle name "${handle}"`);
2481
+ }
2482
+ };
2483
+ if (wc.inputKw) {
2484
+ const handle = wc.inputAlias
2485
+ ? extractNameToken(wc.inputAlias[0])
2486
+ : "input";
2487
+ checkDuplicate(handle);
2488
+ handleBindings.push({ handle, kind: "input" });
2489
+ handleRes.set(handle, {
2490
+ module: SELF_MODULE,
2491
+ type: bridgeType,
2492
+ field: bridgeField,
2493
+ });
2494
+ }
2495
+ else if (wc.outputKw) {
2496
+ const handle = wc.outputAlias
2497
+ ? extractNameToken(wc.outputAlias[0])
2498
+ : "output";
2499
+ checkDuplicate(handle);
2500
+ handleBindings.push({ handle, kind: "output" });
2501
+ handleRes.set(handle, {
2502
+ module: SELF_MODULE,
2503
+ type: bridgeType,
2504
+ field: bridgeField,
2505
+ });
2506
+ }
2507
+ else if (wc.contextKw) {
2508
+ const handle = wc.contextAlias
2509
+ ? extractNameToken(wc.contextAlias[0])
2510
+ : "context";
2511
+ checkDuplicate(handle);
2512
+ handleBindings.push({ handle, kind: "context" });
2513
+ handleRes.set(handle, {
2514
+ module: SELF_MODULE,
2515
+ type: "Context",
2516
+ field: "context",
2517
+ });
2518
+ }
2519
+ else if (wc.constKw) {
2520
+ const handle = wc.constAlias
2521
+ ? extractNameToken(wc.constAlias[0])
2522
+ : "const";
2523
+ checkDuplicate(handle);
2524
+ handleBindings.push({ handle, kind: "const" });
2525
+ handleRes.set(handle, {
2526
+ module: SELF_MODULE,
2527
+ type: "Const",
2528
+ field: "const",
2529
+ });
2530
+ }
2531
+ else if (wc.refName) {
2532
+ const name = extractDottedName(wc.refName[0]);
2533
+ const lastDot = name.lastIndexOf(".");
2534
+ const defaultHandle = lastDot !== -1 ? name.substring(lastDot + 1) : name;
2535
+ const handle = wc.refAlias
2536
+ ? extractNameToken(wc.refAlias[0])
2537
+ : defaultHandle;
2538
+ checkDuplicate(handle);
2539
+ if (wc.refAlias)
2540
+ assertNotReserved(handle, lineNum, "handle alias");
2541
+ // Check if it's a define reference
2542
+ const defineDef = previousInstructions.find((inst) => inst.kind === "define" && inst.name === name);
2543
+ if (defineDef) {
2544
+ handleBindings.push({ handle, kind: "define", name });
2545
+ handleRes.set(handle, {
2546
+ module: `__define_${handle}`,
2547
+ type: bridgeType,
2548
+ field: bridgeField,
2549
+ });
2550
+ }
2551
+ else if (lastDot !== -1) {
2552
+ const modulePart = name.substring(0, lastDot);
2553
+ const fieldPart = name.substring(lastDot + 1);
2554
+ const key = `${modulePart}:${fieldPart}`;
2555
+ const instance = (instanceCounters.get(key) ?? 0) + 1;
2556
+ instanceCounters.set(key, instance);
2557
+ handleBindings.push({ handle, kind: "tool", name });
2558
+ handleRes.set(handle, {
2559
+ module: modulePart,
2560
+ type: bridgeType,
2561
+ field: fieldPart,
2562
+ instance,
2563
+ });
2564
+ }
2565
+ else {
2566
+ const key = `Tools:${name}`;
2567
+ const instance = (instanceCounters.get(key) ?? 0) + 1;
2568
+ instanceCounters.set(key, instance);
2569
+ handleBindings.push({ handle, kind: "tool", name });
2570
+ handleRes.set(handle, {
2571
+ module: SELF_MODULE,
2572
+ type: "Tools",
2573
+ field: name,
2574
+ instance,
2575
+ });
2576
+ }
2577
+ }
2578
+ }
2579
+ // ── Helper: resolve address ────────────────────────────────────────────
2580
+ function resolveAddress(root, segments, lineNum) {
2581
+ const resolution = handleRes.get(root);
2582
+ if (!resolution) {
2583
+ if (segments.length === 0) {
2584
+ throw new Error(`Line ${lineNum}: Undeclared reference "${root}". Add 'with output as o' for output fields, or 'with ${root}' for a tool.`);
2585
+ }
2586
+ throw new Error(`Line ${lineNum}: Undeclared handle "${root}". Add 'with ${root}' or 'with ${root} as ${root}' to the bridge header.`);
2587
+ }
2588
+ const ref = {
2589
+ module: resolution.module,
2590
+ type: resolution.type,
2591
+ field: resolution.field,
2592
+ path: [...segments],
2593
+ };
2594
+ if (resolution.instance != null)
2595
+ ref.instance = resolution.instance;
2596
+ return ref;
2597
+ }
2598
+ function assertNoTargetIndices(ref, lineNum) {
2599
+ if (ref.path.some((seg) => /^\d+$/.test(seg))) {
2600
+ throw new Error(`Line ${lineNum}: Explicit array index in wire target is not supported. Use array mapping (\`[] as iter { }\`) instead.`);
2601
+ }
2602
+ }
2603
+ // ── Helper: process block-scoped with-declarations inside array maps ──
2604
+ /**
2605
+ * Process `with <source> as <alias>` declarations inside an array mapping.
2606
+ * For each declaration:
2607
+ * 1. Build the source ref (iterator-aware: pipe:it becomes a pipe fork ref)
2608
+ * 2. Create a __local trunk for the alias
2609
+ * 3. Register the alias in handleRes so subsequent element lines can reference it
2610
+ * 4. Emit a wire from source to the local trunk
2611
+ *
2612
+ * Returns a cleanup function that removes local aliases from handleRes.
2613
+ */
2614
+ function processLocalBindings(withDecls, iterName) {
2615
+ const addedAliases = [];
2616
+ for (const withDecl of withDecls) {
2617
+ const lineNum = line(findFirstToken(withDecl));
2618
+ const sourceNode = sub(withDecl, "elemWithSource");
2619
+ const alias = extractNameToken(sub(withDecl, "elemWithAlias"));
2620
+ assertNotReserved(alias, lineNum, "local binding alias");
2621
+ if (handleRes.has(alias)) {
2622
+ throw new Error(`Line ${lineNum}: Duplicate handle name "${alias}"`);
2623
+ }
2624
+ // Build source ref — iterator-aware (handles pipe:iter and plain iter refs)
2625
+ const headNode = sub(sourceNode, "head");
2626
+ const pipeSegs = subs(sourceNode, "pipeSegment");
2627
+ const { root: srcRoot, segments: srcSegs } = extractAddressPath(headNode);
2628
+ let sourceRef;
2629
+ if (srcRoot === iterName && pipeSegs.length === 0) {
2630
+ // Iterator-relative plain ref (e.g. `with it.data as d`)
2631
+ sourceRef = {
2632
+ module: SELF_MODULE,
2633
+ type: bridgeType,
2634
+ field: bridgeField,
2635
+ element: true,
2636
+ path: srcSegs,
2637
+ };
2638
+ }
2639
+ else if (pipeSegs.length > 0) {
2640
+ // Pipe expression — the last segment may be iterator-relative.
2641
+ // Resolve data source (last part), then build pipe fork chain.
2642
+ const allParts = [headNode, ...pipeSegs];
2643
+ const actualSourceNode = allParts[allParts.length - 1];
2644
+ const pipeChainNodes = allParts.slice(0, -1);
2645
+ const { root: dataSrcRoot, segments: dataSrcSegs } = extractAddressPath(actualSourceNode);
2646
+ let prevOutRef;
2647
+ if (dataSrcRoot === iterName) {
2648
+ // Iterator-relative pipe source (e.g. `pipe:it` or `pipe:it.field`)
2649
+ prevOutRef = {
2650
+ module: SELF_MODULE,
2651
+ type: bridgeType,
2652
+ field: bridgeField,
2653
+ element: true,
2654
+ path: dataSrcSegs,
2655
+ };
2656
+ }
2657
+ else {
2658
+ prevOutRef = resolveAddress(dataSrcRoot, dataSrcSegs, lineNum);
2659
+ }
2660
+ // Build pipe fork chain (same logic as buildSourceExpr)
2661
+ const reversed = [...pipeChainNodes].reverse();
2662
+ for (let idx = 0; idx < reversed.length; idx++) {
2663
+ const pNode = reversed[idx];
2664
+ const { root: handleName, segments: handleSegs } = extractAddressPath(pNode);
2665
+ if (!handleRes.has(handleName)) {
2666
+ throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${handleName}". Add 'with <tool> as ${handleName}' to the bridge header.`);
2667
+ }
2668
+ const fieldName = handleSegs.length > 0 ? handleSegs.join(".") : "in";
2669
+ const res = handleRes.get(handleName);
2670
+ const forkInstance = 100000 + nextForkSeq++;
2671
+ const forkKey = `${res.module}:${res.type}:${res.field}:${forkInstance}`;
2672
+ pipeHandleEntries.push({
2673
+ key: forkKey,
2674
+ handle: handleName,
2675
+ baseTrunk: {
2676
+ module: res.module,
2677
+ type: res.type,
2678
+ field: res.field,
2679
+ instance: res.instance,
2680
+ },
2681
+ });
2682
+ const forkInRef = {
2683
+ module: res.module,
2684
+ type: res.type,
2685
+ field: res.field,
2686
+ instance: forkInstance,
2687
+ path: parsePath(fieldName),
2688
+ };
2689
+ const forkRootRef = {
2690
+ module: res.module,
2691
+ type: res.type,
2692
+ field: res.field,
2693
+ instance: forkInstance,
2694
+ path: [],
2695
+ };
2696
+ wires.push({
2697
+ from: prevOutRef,
2698
+ to: forkInRef,
2699
+ pipe: true,
2700
+ });
2701
+ prevOutRef = forkRootRef;
2702
+ }
2703
+ sourceRef = prevOutRef;
2704
+ }
2705
+ else {
2706
+ sourceRef = buildSourceExpr(sourceNode, lineNum);
2707
+ }
2708
+ // Create __local trunk for the alias
2709
+ const localRes = {
2710
+ module: "__local",
2711
+ type: "Shadow",
2712
+ field: alias,
2713
+ };
2714
+ handleRes.set(alias, localRes);
2715
+ addedAliases.push(alias);
2716
+ // Emit wire from source to local trunk
2717
+ const localToRef = {
2718
+ module: "__local",
2719
+ type: "Shadow",
2720
+ field: alias,
2721
+ path: [],
2722
+ };
2723
+ wires.push({ from: sourceRef, to: localToRef });
2724
+ }
2725
+ return () => {
2726
+ for (const alias of addedAliases) {
2727
+ handleRes.delete(alias);
2728
+ }
2729
+ };
2730
+ }
2731
+ // ── Helper: build source expression ────────────────────────────────────
2732
+ function buildSourceExprSafe(sourceNode, lineNum) {
2733
+ const headNode = sub(sourceNode, "head");
2734
+ const pipeNodes = subs(sourceNode, "pipeSegment");
2735
+ if (pipeNodes.length === 0) {
2736
+ const { root, segments, safe, rootSafe, segmentSafe } = extractAddressPath(headNode);
2737
+ const ref = resolveAddress(root, segments, lineNum);
2738
+ return {
2739
+ ref: {
2740
+ ...ref,
2741
+ ...(rootSafe ? { rootSafe: true } : {}),
2742
+ ...(segmentSafe ? { pathSafe: segmentSafe } : {}),
2743
+ },
2744
+ safe,
2745
+ };
2746
+ }
2747
+ // Pipe chain: all parts in order [head, ...pipeSegments]
2748
+ // The LAST part is the actual data source; everything before is a pipe handle.
2749
+ const allParts = [headNode, ...pipeNodes];
2750
+ const actualSourceNode = allParts[allParts.length - 1];
2751
+ const pipeChainNodes = allParts.slice(0, -1);
2752
+ // Validate all pipe handles
2753
+ for (const pipeNode of pipeChainNodes) {
2754
+ const { root } = extractAddressPath(pipeNode);
2755
+ if (!handleRes.has(root)) {
2756
+ throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${root}". Add 'with <tool> as ${root}' to the bridge header.`);
2757
+ }
2758
+ }
2759
+ const { root: srcRoot, segments: srcSegments, safe, rootSafe: srcRootSafe, segmentSafe: srcSegmentSafe, } = extractAddressPath(actualSourceNode);
2760
+ let prevOutRef = resolveAddress(srcRoot, srcSegments, lineNum);
2761
+ // Process pipe handles right-to-left (innermost first)
2762
+ const reversed = [...pipeChainNodes].reverse();
2763
+ for (let idx = 0; idx < reversed.length; idx++) {
2764
+ const pNode = reversed[idx];
2765
+ const { root: handleName, segments: handleSegs } = extractAddressPath(pNode);
2766
+ const fieldName = handleSegs.length > 0 ? handleSegs.join(".") : "in";
2767
+ const res = handleRes.get(handleName);
2768
+ const forkInstance = 100000 + nextForkSeq++;
2769
+ const forkKey = `${res.module}:${res.type}:${res.field}:${forkInstance}`;
2770
+ pipeHandleEntries.push({
2771
+ key: forkKey,
2772
+ handle: handleName,
2773
+ baseTrunk: {
2774
+ module: res.module,
2775
+ type: res.type,
2776
+ field: res.field,
2777
+ instance: res.instance,
2778
+ },
2779
+ });
2780
+ const forkInRef = {
2781
+ module: res.module,
2782
+ type: res.type,
2783
+ field: res.field,
2784
+ instance: forkInstance,
2785
+ path: parsePath(fieldName),
2786
+ };
2787
+ const forkRootRef = {
2788
+ module: res.module,
2789
+ type: res.type,
2790
+ field: res.field,
2791
+ instance: forkInstance,
2792
+ path: [],
2793
+ };
2794
+ wires.push({
2795
+ from: prevOutRef,
2796
+ to: forkInRef,
2797
+ pipe: true,
2798
+ });
2799
+ prevOutRef = forkRootRef;
2800
+ }
2801
+ return {
2802
+ ref: {
2803
+ ...prevOutRef,
2804
+ ...(srcRootSafe ? { rootSafe: true } : {}),
2805
+ ...(srcSegmentSafe ? { pathSafe: srcSegmentSafe } : {}),
2806
+ },
2807
+ safe,
2808
+ };
2809
+ }
2810
+ /** Backward-compat wrapper — returns just the NodeRef. */
2811
+ function buildSourceExpr(sourceNode, lineNum) {
2812
+ return buildSourceExprSafe(sourceNode, lineNum).ref;
2813
+ }
2814
+ // ── Helper: desugar template string into synthetic internal.concat fork ─────
2815
+ function desugarTemplateString(segs, lineNum, iterName) {
2816
+ const forkInstance = 100000 + nextForkSeq++;
2817
+ const forkModule = SELF_MODULE;
2818
+ const forkType = "Tools";
2819
+ const forkField = "concat";
2820
+ const forkKey = `${forkModule}:${forkType}:${forkField}:${forkInstance}`;
2821
+ pipeHandleEntries.push({
2822
+ key: forkKey,
2823
+ handle: `__concat_${forkInstance}`,
2824
+ baseTrunk: {
2825
+ module: forkModule,
2826
+ type: forkType,
2827
+ field: forkField,
2828
+ },
2829
+ });
2830
+ for (let idx = 0; idx < segs.length; idx++) {
2831
+ const seg = segs[idx];
2832
+ const partRef = {
2833
+ module: forkModule,
2834
+ type: forkType,
2835
+ field: forkField,
2836
+ instance: forkInstance,
2837
+ path: ["parts", String(idx)],
2838
+ };
2839
+ if (seg.kind === "text") {
2840
+ wires.push({ value: seg.value, to: partRef });
2841
+ }
2842
+ else {
2843
+ // Parse the ref path: e.g. "i.id" → root="i", segments=["id"]
2844
+ const dotParts = seg.path.split(".");
2845
+ const root = dotParts[0];
2846
+ const segments = dotParts.slice(1);
2847
+ // Check for iterator-relative refs
2848
+ if (iterName && root === iterName) {
2849
+ const fromRef = {
2850
+ module: SELF_MODULE,
2851
+ type: bridgeType,
2852
+ field: bridgeField,
2853
+ element: true,
2854
+ path: segments,
2855
+ };
2856
+ wires.push({ from: fromRef, to: partRef });
2857
+ }
2858
+ else {
2859
+ const fromRef = resolveAddress(root, segments, lineNum);
2860
+ wires.push({ from: fromRef, to: partRef });
2861
+ }
2862
+ }
2863
+ }
2864
+ return {
2865
+ module: forkModule,
2866
+ type: forkType,
2867
+ field: forkField,
2868
+ instance: forkInstance,
2869
+ path: ["value"],
2870
+ };
2871
+ }
2872
+ // ── Helper: extract coalesce alternative ───────────────────────────────
2873
+ function extractCoalesceAlt(altNode, lineNum, iterName) {
2874
+ const c = altNode.children;
2875
+ // Control flow keywords
2876
+ if (c.throwKw) {
2877
+ const msg = c.throwMsg[0].image;
2878
+ return { control: { kind: "throw", message: JSON.parse(msg) } };
2879
+ }
2880
+ if (c.panicKw) {
2881
+ const msg = c.panicMsg[0].image;
2882
+ return { control: { kind: "panic", message: JSON.parse(msg) } };
2883
+ }
2884
+ if (c.continueKw)
2885
+ return { control: { kind: "continue" } };
2886
+ if (c.breakKw)
2887
+ return { control: { kind: "break" } };
2888
+ if (c.stringLit) {
2889
+ const raw = c.stringLit[0].image;
2890
+ const segs = parseTemplateString(raw.slice(1, -1));
2891
+ if (segs)
2892
+ return { sourceRef: desugarTemplateString(segs, lineNum, iterName) };
2893
+ return { literal: raw };
2894
+ }
2895
+ if (c.numberLit)
2896
+ return { literal: c.numberLit[0].image };
2897
+ if (c.intLit)
2898
+ return { literal: c.intLit[0].image };
2899
+ if (c.trueLit)
2900
+ return { literal: "true" };
2901
+ if (c.falseLit)
2902
+ return { literal: "false" };
2903
+ if (c.nullLit)
2904
+ return { literal: "null" };
2905
+ if (c.objectLit)
2906
+ return { literal: reconstructJson(c.objectLit[0]) };
2907
+ if (c.sourceAlt) {
2908
+ const srcNode = c.sourceAlt[0];
2909
+ return { sourceRef: buildSourceExpr(srcNode, lineNum) };
2910
+ }
2911
+ throw new Error(`Line ${lineNum}: Invalid coalesce alternative`);
2912
+ }
2913
+ // ── Helper: extract ternary branch ────────────────────────────────────
2914
+ /**
2915
+ * Resolve a ternaryBranch CST node to either a NodeRef (source) or a
2916
+ * raw literal string suitable for JSON.parse (kept verbatim for numbers
2917
+ * / booleans / null; kept with quotes for strings so JSON.parse works).
2918
+ */
2919
+ function extractTernaryBranch(branchNode, lineNum, iterName) {
2920
+ const c = branchNode.children;
2921
+ if (c.stringLit) {
2922
+ const raw = c.stringLit[0].image;
2923
+ const segs = parseTemplateString(raw.slice(1, -1));
2924
+ if (segs)
2925
+ return {
2926
+ kind: "ref",
2927
+ ref: desugarTemplateString(segs, lineNum, iterName),
2928
+ };
2929
+ return { kind: "literal", value: raw };
2930
+ }
2931
+ if (c.numberLit)
2932
+ return { kind: "literal", value: c.numberLit[0].image };
2933
+ if (c.trueLit)
2934
+ return { kind: "literal", value: "true" };
2935
+ if (c.falseLit)
2936
+ return { kind: "literal", value: "false" };
2937
+ if (c.nullLit)
2938
+ return { kind: "literal", value: "null" };
2939
+ if (c.sourceRef) {
2940
+ const addrNode = c.sourceRef[0];
2941
+ const { root, segments } = extractAddressPath(addrNode);
2942
+ // Iterator-relative ref in element context
2943
+ if (iterName && root === iterName) {
2944
+ return {
2945
+ kind: "ref",
2946
+ ref: {
2947
+ module: SELF_MODULE,
2948
+ type: bridgeType,
2949
+ field: bridgeField,
2950
+ element: true,
2951
+ path: segments,
2952
+ },
2953
+ };
2954
+ }
2955
+ return { kind: "ref", ref: resolveAddress(root, segments, lineNum) };
2956
+ }
2957
+ throw new Error(`Line ${lineNum}: Invalid ternary branch`);
2958
+ }
2959
+ // ── Helper: operator symbol → std tool function name ──────────────────
2960
+ /** Map infix operator token to the std tool that implements it. */
2961
+ const OP_TO_FN = {
2962
+ "*": "multiply",
2963
+ "/": "divide",
2964
+ "+": "add",
2965
+ "-": "subtract",
2966
+ "==": "eq",
2967
+ "!=": "neq",
2968
+ ">": "gt",
2969
+ ">=": "gte",
2970
+ "<": "lt",
2971
+ "<=": "lte",
2972
+ // and/or are handled as native condAnd/condOr wires, not tool forks
2973
+ };
2974
+ /** Operator precedence: higher number = binds tighter. */
2975
+ const OP_PREC = {
2976
+ "*": 4,
2977
+ "/": 4,
2978
+ "+": 3,
2979
+ "-": 3,
2980
+ "==": 2,
2981
+ "!=": 2,
2982
+ ">": 2,
2983
+ ">=": 2,
2984
+ "<": 2,
2985
+ "<=": 2,
2986
+ and: 1,
2987
+ or: 0,
2988
+ };
2989
+ function extractExprOpStr(opNode) {
2990
+ const c = opNode.children;
2991
+ if (c.star)
2992
+ return "*";
2993
+ if (c.slash)
2994
+ return "/";
2995
+ if (c.plus)
2996
+ return "+";
2997
+ if (c.minus)
2998
+ return "-";
2999
+ if (c.doubleEquals)
3000
+ return "==";
3001
+ if (c.notEquals)
3002
+ return "!=";
3003
+ if (c.greaterEqual)
3004
+ return ">=";
3005
+ if (c.lessEqual)
3006
+ return "<=";
3007
+ if (c.greaterThan)
3008
+ return ">";
3009
+ if (c.lessThan)
3010
+ return "<";
3011
+ if (c.andKw)
3012
+ return "and";
3013
+ if (c.orKw)
3014
+ return "or";
3015
+ throw new Error("Invalid expression operator");
3016
+ }
3017
+ /**
3018
+ * Resolve an exprOperand CST node to either a NodeRef (source) or
3019
+ * a literal string value suitable for a constant wire.
3020
+ */
3021
+ function resolveExprOperand(operandNode, lineNum, iterName) {
3022
+ const c = operandNode.children;
3023
+ if (c.numberLit)
3024
+ return { kind: "literal", value: c.numberLit[0].image };
3025
+ if (c.stringLit) {
3026
+ const raw = c.stringLit[0].image;
3027
+ const content = raw.slice(1, -1);
3028
+ const segs = parseTemplateString(content);
3029
+ if (segs)
3030
+ return {
3031
+ kind: "ref",
3032
+ ref: desugarTemplateString(segs, lineNum, iterName),
3033
+ };
3034
+ return { kind: "literal", value: content };
3035
+ }
3036
+ if (c.trueLit)
3037
+ return { kind: "literal", value: "1" };
3038
+ if (c.falseLit)
3039
+ return { kind: "literal", value: "0" };
3040
+ if (c.nullLit)
3041
+ return { kind: "literal", value: "0" };
3042
+ if (c.sourceRef) {
3043
+ const srcNode = c.sourceRef[0];
3044
+ // Check for element/iterator-relative refs
3045
+ if (iterName) {
3046
+ const headNode = sub(srcNode, "head");
3047
+ const pipeSegs = subs(srcNode, "pipeSegment");
3048
+ const { root, segments, safe } = extractAddressPath(headNode);
3049
+ if (root === iterName && pipeSegs.length === 0) {
3050
+ return {
3051
+ kind: "ref",
3052
+ safe,
3053
+ ref: {
3054
+ module: SELF_MODULE,
3055
+ type: bridgeType,
3056
+ field: bridgeField,
3057
+ element: true,
3058
+ path: segments,
3059
+ },
3060
+ };
3061
+ }
3062
+ }
3063
+ const { ref, safe } = buildSourceExprSafe(srcNode, lineNum);
3064
+ return { kind: "ref", ref, safe };
3065
+ }
3066
+ if (c.parenExpr) {
3067
+ const parenNode = c.parenExpr[0];
3068
+ const ref = resolveParenExpr(parenNode, lineNum, iterName);
3069
+ return { kind: "ref", ref };
3070
+ }
3071
+ throw new Error(`Line ${lineNum}: Invalid expression operand`);
3072
+ }
3073
+ /**
3074
+ * Resolve a parenthesized sub-expression `( [not] source [op operand]* )`
3075
+ * into a single NodeRef by recursively desugaring the inner chain.
3076
+ */
3077
+ function resolveParenExpr(parenNode, lineNum, iterName, safe) {
3078
+ const pc = parenNode.children;
3079
+ const innerSourceNode = sub(parenNode, "parenSource");
3080
+ const innerOps = subs(parenNode, "parenExprOp");
3081
+ const innerRights = subs(parenNode, "parenExprRight");
3082
+ const hasNot = !!pc.parenNotPrefix?.length;
3083
+ // Build the inner source ref (handling iterator-relative refs)
3084
+ let innerRef;
3085
+ let innerSafe = safe;
3086
+ if (iterName) {
3087
+ const headNode = sub(innerSourceNode, "head");
3088
+ const pipeSegs = subs(innerSourceNode, "pipeSegment");
3089
+ const { root, segments, safe: srcSafe } = extractAddressPath(headNode);
3090
+ if (root === iterName && pipeSegs.length === 0) {
3091
+ innerRef = {
3092
+ module: SELF_MODULE,
3093
+ type: bridgeType,
3094
+ field: bridgeField,
3095
+ element: true,
3096
+ path: segments,
3097
+ };
3098
+ if (srcSafe)
3099
+ innerSafe = true;
3100
+ }
3101
+ else {
3102
+ const result = buildSourceExprSafe(innerSourceNode, lineNum);
3103
+ innerRef = result.ref;
3104
+ if (result.safe)
3105
+ innerSafe = true;
3106
+ }
3107
+ }
3108
+ else {
3109
+ const result = buildSourceExprSafe(innerSourceNode, lineNum);
3110
+ innerRef = result.ref;
3111
+ if (result.safe)
3112
+ innerSafe = true;
3113
+ }
3114
+ // Desugar the inner expression chain if there are operators
3115
+ let resultRef;
3116
+ if (innerOps.length > 0) {
3117
+ resultRef = desugarExprChain(innerRef, innerOps, innerRights, lineNum, iterName, innerSafe);
3118
+ }
3119
+ else {
3120
+ resultRef = innerRef;
3121
+ }
3122
+ // Apply not prefix if present
3123
+ if (hasNot) {
3124
+ resultRef = desugarNot(resultRef, lineNum, innerSafe);
3125
+ }
3126
+ return resultRef;
3127
+ }
3128
+ /**
3129
+ * Desugar an infix expression chain into synthetic tool wires,
3130
+ * respecting operator precedence (* / before + - before comparisons).
3131
+ *
3132
+ * Given: leftRef + rightA * rightB > 5
3133
+ * Produces: leftRef + (rightA * rightB) > 5
3134
+ *
3135
+ * Each binary node creates a synthetic tool fork (like pipe desugaring):
3136
+ * __expr fork instance → { a: left, b: right } → result
3137
+ */
3138
+ function desugarExprChain(leftRef, exprOps, exprRights, lineNum, iterName, safe) {
3139
+ const operands = [{ kind: "ref", ref: leftRef, safe }];
3140
+ const ops = [];
3141
+ for (let i = 0; i < exprOps.length; i++) {
3142
+ ops.push(extractExprOpStr(exprOps[i]));
3143
+ operands.push(resolveExprOperand(exprRights[i], lineNum, iterName));
3144
+ }
3145
+ // Emit a synthetic fork for a single binary operation and return
3146
+ // an operand pointing to the fork's result.
3147
+ function emitFork(left, opStr, right) {
3148
+ // Derive safe flag per operand
3149
+ const leftSafe = left.kind === "ref" && !!left.safe;
3150
+ const rightSafe = right.kind === "ref" && !!right.safe;
3151
+ // ── Short-circuit and/or: emit condAnd/condOr wire ──
3152
+ if (opStr === "and" || opStr === "or") {
3153
+ const forkInstance = 100000 + nextForkSeq++;
3154
+ const forkField = opStr === "and" ? "__and" : "__or";
3155
+ const forkTrunkModule = SELF_MODULE;
3156
+ const forkTrunkType = "Tools";
3157
+ const forkKey = `${forkTrunkModule}:${forkTrunkType}:${forkField}:${forkInstance}`;
3158
+ pipeHandleEntries.push({
3159
+ key: forkKey,
3160
+ handle: `__expr_${forkInstance}`,
3161
+ baseTrunk: {
3162
+ module: forkTrunkModule,
3163
+ type: forkTrunkType,
3164
+ field: forkField,
3165
+ },
3166
+ });
3167
+ const toRef = {
3168
+ module: forkTrunkModule,
3169
+ type: forkTrunkType,
3170
+ field: forkField,
3171
+ instance: forkInstance,
3172
+ path: [],
3173
+ };
3174
+ // Build the leftRef for the condAnd/condOr
3175
+ const leftRef = left.kind === "ref"
3176
+ ? left.ref
3177
+ : (() => {
3178
+ // Literal left: emit a constant wire and reference it
3179
+ const litInstance = 100000 + nextForkSeq++;
3180
+ const litField = "__lit";
3181
+ const litKey = `${forkTrunkModule}:${forkTrunkType}:${litField}:${litInstance}`;
3182
+ pipeHandleEntries.push({
3183
+ key: litKey,
3184
+ handle: `__expr_${litInstance}`,
3185
+ baseTrunk: {
3186
+ module: forkTrunkModule,
3187
+ type: forkTrunkType,
3188
+ field: litField,
3189
+ },
3190
+ });
3191
+ const litRef = {
3192
+ module: forkTrunkModule,
3193
+ type: forkTrunkType,
3194
+ field: litField,
3195
+ instance: litInstance,
3196
+ path: [],
3197
+ };
3198
+ wires.push({ value: left.value, to: litRef });
3199
+ return litRef;
3200
+ })();
3201
+ // Build right side
3202
+ const rightSide = right.kind === "ref"
3203
+ ? { rightRef: right.ref }
3204
+ : { rightValue: right.value };
3205
+ const safeAttr = leftSafe ? { safe: true } : {};
3206
+ const rightSafeAttr = rightSafe ? { rightSafe: true } : {};
3207
+ if (opStr === "and") {
3208
+ wires.push({
3209
+ condAnd: { leftRef, ...rightSide, ...safeAttr, ...rightSafeAttr },
3210
+ to: toRef,
3211
+ });
3212
+ }
3213
+ else {
3214
+ wires.push({
3215
+ condOr: { leftRef, ...rightSide, ...safeAttr, ...rightSafeAttr },
3216
+ to: toRef,
3217
+ });
3218
+ }
3219
+ return { kind: "ref", ref: toRef };
3220
+ }
3221
+ // ── Standard math/comparison: emit synthetic tool fork ──
3222
+ const fnName = OP_TO_FN[opStr];
3223
+ if (!fnName)
3224
+ throw new Error(`Line ${lineNum}: Unknown operator "${opStr}"`);
3225
+ const forkInstance = 100000 + nextForkSeq++;
3226
+ const forkTrunkModule = SELF_MODULE;
3227
+ const forkTrunkType = "Tools";
3228
+ const forkTrunkField = fnName;
3229
+ const forkKey = `${forkTrunkModule}:${forkTrunkType}:${forkTrunkField}:${forkInstance}`;
3230
+ pipeHandleEntries.push({
3231
+ key: forkKey,
3232
+ handle: `__expr_${forkInstance}`,
3233
+ baseTrunk: {
3234
+ module: forkTrunkModule,
3235
+ type: forkTrunkType,
3236
+ field: forkTrunkField,
3237
+ },
3238
+ });
3239
+ const makeTarget = (slot) => ({
3240
+ module: forkTrunkModule,
3241
+ type: forkTrunkType,
3242
+ field: forkTrunkField,
3243
+ instance: forkInstance,
3244
+ path: [slot],
3245
+ });
3246
+ // Wire left → fork.a (propagate safe flag from operand)
3247
+ if (left.kind === "literal") {
3248
+ wires.push({ value: left.value, to: makeTarget("a") });
3249
+ }
3250
+ else {
3251
+ const safeAttr = leftSafe ? { safe: true } : {};
3252
+ wires.push({
3253
+ from: left.ref,
3254
+ to: makeTarget("a"),
3255
+ pipe: true,
3256
+ ...safeAttr,
3257
+ });
3258
+ }
3259
+ // Wire right → fork.b (propagate safe flag from operand)
3260
+ if (right.kind === "literal") {
3261
+ wires.push({ value: right.value, to: makeTarget("b") });
3262
+ }
3263
+ else {
3264
+ const safeAttr = rightSafe ? { safe: true } : {};
3265
+ wires.push({
3266
+ from: right.ref,
3267
+ to: makeTarget("b"),
3268
+ pipe: true,
3269
+ ...safeAttr,
3270
+ });
3271
+ }
3272
+ return {
3273
+ kind: "ref",
3274
+ ref: {
3275
+ module: forkTrunkModule,
3276
+ type: forkTrunkType,
3277
+ field: forkTrunkField,
3278
+ instance: forkInstance,
3279
+ path: [],
3280
+ },
3281
+ };
3282
+ }
3283
+ // Reduce all operators at a given precedence level (left-to-right).
3284
+ // Modifies operands/ops arrays in place, collapsing matched pairs.
3285
+ function reduceLevel(prec) {
3286
+ let i = 0;
3287
+ while (i < ops.length) {
3288
+ if ((OP_PREC[ops[i]] ?? 0) === prec) {
3289
+ const result = emitFork(operands[i], ops[i], operands[i + 1]);
3290
+ operands.splice(i, 2, result);
3291
+ ops.splice(i, 1);
3292
+ }
3293
+ else {
3294
+ i++;
3295
+ }
3296
+ }
3297
+ }
3298
+ // Process in precedence order: * / first, then + -, then comparisons, then and, then or.
3299
+ reduceLevel(4); // * /
3300
+ reduceLevel(3); // + -
3301
+ reduceLevel(2); // == != > >= < <=
3302
+ reduceLevel(1); // and
3303
+ reduceLevel(0); // or
3304
+ // After full reduction, operands[0] holds the final result.
3305
+ const final = operands[0];
3306
+ if (final.kind !== "ref") {
3307
+ throw new Error(`Line ${lineNum}: Expression must contain at least one source reference`);
3308
+ }
3309
+ return final.ref;
3310
+ }
3311
+ /**
3312
+ * Desugar a `not` prefix into a synthetic unary fork that calls `internal.not`.
3313
+ * Wraps the given ref: not(sourceRef) → __expr fork with { a: sourceRef }
3314
+ */
3315
+ function desugarNot(sourceRef, _lineNum, safe) {
3316
+ const forkInstance = 100000 + nextForkSeq++;
3317
+ const forkTrunkModule = SELF_MODULE;
3318
+ const forkTrunkType = "Tools";
3319
+ const forkTrunkField = "not";
3320
+ const forkKey = `${forkTrunkModule}:${forkTrunkType}:${forkTrunkField}:${forkInstance}`;
3321
+ pipeHandleEntries.push({
3322
+ key: forkKey,
3323
+ handle: `__expr_${forkInstance}`,
3324
+ baseTrunk: {
3325
+ module: forkTrunkModule,
3326
+ type: forkTrunkType,
3327
+ field: forkTrunkField,
3328
+ },
3329
+ });
3330
+ const safeAttr = safe ? { safe: true } : {};
3331
+ wires.push({
3332
+ from: sourceRef,
3333
+ to: {
3334
+ module: forkTrunkModule,
3335
+ type: forkTrunkType,
3336
+ field: forkTrunkField,
3337
+ instance: forkInstance,
3338
+ path: ["a"],
3339
+ },
3340
+ pipe: true,
3341
+ ...safeAttr,
3342
+ });
3343
+ return {
3344
+ module: forkTrunkModule,
3345
+ type: forkTrunkType,
3346
+ field: forkTrunkField,
3347
+ instance: forkInstance,
3348
+ path: [],
3349
+ };
3350
+ }
3351
+ // ── Helper: recursively process path scoping block lines ───────────────
3352
+ // Flattens nested scope blocks into standard flat wires by prepending
3353
+ // the accumulated path prefix to each inner target.
3354
+ function processScopeLines(scopeLines, targetRoot, pathPrefix) {
3355
+ for (const scopeLine of scopeLines) {
3356
+ const sc = scopeLine.children;
3357
+ const scopeLineNum = line(findFirstToken(scopeLine));
3358
+ const targetStr = extractDottedPathStr(sub(scopeLine, "scopeTarget"));
3359
+ const scopeSegs = parsePath(targetStr);
3360
+ const fullSegs = [...pathPrefix, ...scopeSegs];
3361
+ // ── Nested scope: .field { ... } ──
3362
+ const nestedScopeLines = subs(scopeLine, "pathScopeLine");
3363
+ if (nestedScopeLines.length > 0 && !sc.scopeEquals && !sc.scopeArrow) {
3364
+ // Process alias declarations inside the nested scope block first
3365
+ const scopeAliases = subs(scopeLine, "scopeAlias");
3366
+ for (const aliasNode of scopeAliases) {
3367
+ const aliasLineNum = line(findFirstToken(aliasNode));
3368
+ const sourceNode = sub(aliasNode, "nodeAliasSource");
3369
+ const alias = extractNameToken(sub(aliasNode, "nodeAliasName"));
3370
+ assertNotReserved(alias, aliasLineNum, "node alias");
3371
+ if (handleRes.has(alias)) {
3372
+ throw new Error(`Line ${aliasLineNum}: Duplicate handle name "${alias}"`);
3373
+ }
3374
+ const { ref: sourceRef, safe: aliasSafe } = buildSourceExprSafe(sourceNode, aliasLineNum);
3375
+ const localRes = {
3376
+ module: "__local",
3377
+ type: "Shadow",
3378
+ field: alias,
3379
+ };
3380
+ handleRes.set(alias, localRes);
3381
+ const localToRef = {
3382
+ module: "__local",
3383
+ type: "Shadow",
3384
+ field: alias,
3385
+ path: [],
3386
+ };
3387
+ wires.push({
3388
+ from: sourceRef,
3389
+ to: localToRef,
3390
+ ...(aliasSafe ? { safe: true } : {}),
3391
+ });
3392
+ }
3393
+ processScopeLines(nestedScopeLines, targetRoot, fullSegs);
3394
+ continue;
3395
+ }
3396
+ const toRef = resolveAddress(targetRoot, fullSegs, scopeLineNum);
3397
+ assertNoTargetIndices(toRef, scopeLineNum);
3398
+ // ── Constant wire: .field = value ──
3399
+ if (sc.scopeEquals) {
3400
+ const value = extractBareValue(sub(scopeLine, "scopeValue"));
3401
+ wires.push({ value, to: toRef });
3402
+ continue;
3403
+ }
3404
+ // ── Pull wire: .field <- source [modifiers] ──
3405
+ if (sc.scopeArrow) {
3406
+ // String source (template or plain): .field <- "..."
3407
+ const stringSourceToken = sc.scopeStringSource?.[0];
3408
+ if (stringSourceToken) {
3409
+ const raw = stringSourceToken.image.slice(1, -1);
3410
+ const segs = parseTemplateString(raw);
3411
+ let falsyFallback;
3412
+ let falsyControl;
3413
+ const nullAltRefs = [];
3414
+ for (const alt of subs(scopeLine, "scopeNullAlt")) {
3415
+ const altResult = extractCoalesceAlt(alt, scopeLineNum);
3416
+ if ("literal" in altResult)
3417
+ falsyFallback = altResult.literal;
3418
+ else if ("control" in altResult)
3419
+ falsyControl = altResult.control;
3420
+ else
3421
+ nullAltRefs.push(altResult.sourceRef);
3422
+ }
3423
+ let nullishFallback;
3424
+ let nullishControl;
3425
+ let nullishFallbackRef;
3426
+ let nullishFallbackInternalWires = [];
3427
+ const nullishAlt = sub(scopeLine, "scopeNullishAlt");
3428
+ if (nullishAlt) {
3429
+ const preLen = wires.length;
3430
+ const altResult = extractCoalesceAlt(nullishAlt, scopeLineNum);
3431
+ if ("literal" in altResult)
3432
+ nullishFallback = altResult.literal;
3433
+ else if ("control" in altResult)
3434
+ nullishControl = altResult.control;
3435
+ else {
3436
+ nullishFallbackRef = altResult.sourceRef;
3437
+ nullishFallbackInternalWires = wires.splice(preLen);
3438
+ }
3439
+ }
3440
+ let catchFallback;
3441
+ let catchControl;
3442
+ let catchFallbackRef;
3443
+ let catchFallbackInternalWires = [];
3444
+ const catchAlt = sub(scopeLine, "scopeCatchAlt");
3445
+ if (catchAlt) {
3446
+ const preLen = wires.length;
3447
+ const altResult = extractCoalesceAlt(catchAlt, scopeLineNum);
3448
+ if ("literal" in altResult)
3449
+ catchFallback = altResult.literal;
3450
+ else if ("control" in altResult)
3451
+ catchControl = altResult.control;
3452
+ else {
3453
+ catchFallbackRef = altResult.sourceRef;
3454
+ catchFallbackInternalWires = wires.splice(preLen);
3455
+ }
3456
+ }
3457
+ const lastAttrs = {
3458
+ ...(nullAltRefs.length > 0
3459
+ ? { falsyFallbackRefs: nullAltRefs }
3460
+ : {}),
3461
+ ...(falsyFallback ? { falsyFallback } : {}),
3462
+ ...(falsyControl ? { falsyControl } : {}),
3463
+ ...(nullishFallback ? { nullishFallback } : {}),
3464
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
3465
+ ...(nullishControl ? { nullishControl } : {}),
3466
+ ...(catchFallback ? { catchFallback } : {}),
3467
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
3468
+ ...(catchControl ? { catchControl } : {}),
3469
+ };
3470
+ if (segs) {
3471
+ const concatOutRef = desugarTemplateString(segs, scopeLineNum);
3472
+ wires.push({
3473
+ from: concatOutRef,
3474
+ to: toRef,
3475
+ pipe: true,
3476
+ ...lastAttrs,
3477
+ });
3478
+ }
3479
+ else {
3480
+ wires.push({ value: raw, to: toRef, ...lastAttrs });
3481
+ }
3482
+ wires.push(...nullishFallbackInternalWires);
3483
+ wires.push(...catchFallbackInternalWires);
3484
+ continue;
3485
+ }
3486
+ // Normal source expression
3487
+ const firstSourceNode = sub(scopeLine, "scopeSource");
3488
+ const scopeFirstParenNode = sub(scopeLine, "scopeFirstParenExpr");
3489
+ const sourceParts = [];
3490
+ const exprOps = subs(scopeLine, "scopeExprOp");
3491
+ // Extract safe flag from head node
3492
+ let scopeBlockSafe = false;
3493
+ if (firstSourceNode) {
3494
+ const headNode = sub(firstSourceNode, "head");
3495
+ if (headNode) {
3496
+ scopeBlockSafe = !!extractAddressPath(headNode).safe;
3497
+ }
3498
+ }
3499
+ let condRef;
3500
+ let condIsPipeFork;
3501
+ if (scopeFirstParenNode) {
3502
+ const parenRef = resolveParenExpr(scopeFirstParenNode, scopeLineNum, undefined, scopeBlockSafe || undefined);
3503
+ if (exprOps.length > 0) {
3504
+ const exprRights = subs(scopeLine, "scopeExprRight");
3505
+ condRef = desugarExprChain(parenRef, exprOps, exprRights, scopeLineNum, undefined, scopeBlockSafe || undefined);
3506
+ }
3507
+ else {
3508
+ condRef = parenRef;
3509
+ }
3510
+ condIsPipeFork = true;
3511
+ }
3512
+ else if (exprOps.length > 0) {
3513
+ const exprRights = subs(scopeLine, "scopeExprRight");
3514
+ const leftRef = buildSourceExpr(firstSourceNode, scopeLineNum);
3515
+ condRef = desugarExprChain(leftRef, exprOps, exprRights, scopeLineNum, undefined, scopeBlockSafe || undefined);
3516
+ condIsPipeFork = true;
3517
+ }
3518
+ else {
3519
+ const pipeSegs = subs(firstSourceNode, "pipeSegment");
3520
+ condRef = buildSourceExpr(firstSourceNode, scopeLineNum);
3521
+ condIsPipeFork =
3522
+ condRef.instance != null &&
3523
+ condRef.path.length === 0 &&
3524
+ pipeSegs.length > 0;
3525
+ }
3526
+ // ── Apply `not` prefix if present (scope context) ──
3527
+ if (sc.scopeNotPrefix?.[0]) {
3528
+ condRef = desugarNot(condRef, scopeLineNum, scopeBlockSafe || undefined);
3529
+ condIsPipeFork = true;
3530
+ }
3531
+ // Ternary wire: .field <- cond ? then : else
3532
+ const scopeTernaryOp = sc.scopeTernaryOp?.[0];
3533
+ if (scopeTernaryOp) {
3534
+ const thenNode = sub(scopeLine, "scopeThenBranch");
3535
+ const elseNode = sub(scopeLine, "scopeElseBranch");
3536
+ const thenBranch = extractTernaryBranch(thenNode, scopeLineNum);
3537
+ const elseBranch = extractTernaryBranch(elseNode, scopeLineNum);
3538
+ let falsyFallback;
3539
+ let falsyControl;
3540
+ const nullAltRefs = [];
3541
+ for (const alt of subs(scopeLine, "scopeNullAlt")) {
3542
+ const altResult = extractCoalesceAlt(alt, scopeLineNum);
3543
+ if ("literal" in altResult)
3544
+ falsyFallback = altResult.literal;
3545
+ else if ("control" in altResult)
3546
+ falsyControl = altResult.control;
3547
+ else
3548
+ nullAltRefs.push(altResult.sourceRef);
3549
+ }
3550
+ let nullishFallback;
3551
+ let nullishControl;
3552
+ let nullishFallbackRef;
3553
+ let nullishFallbackInternalWires = [];
3554
+ const nullishAlt = sub(scopeLine, "scopeNullishAlt");
3555
+ if (nullishAlt) {
3556
+ const preLen = wires.length;
3557
+ const altResult = extractCoalesceAlt(nullishAlt, scopeLineNum);
3558
+ if ("literal" in altResult)
3559
+ nullishFallback = altResult.literal;
3560
+ else if ("control" in altResult)
3561
+ nullishControl = altResult.control;
3562
+ else {
3563
+ nullishFallbackRef = altResult.sourceRef;
3564
+ nullishFallbackInternalWires = wires.splice(preLen);
3565
+ }
3566
+ }
3567
+ let catchFallback;
3568
+ let catchControl;
3569
+ let catchFallbackRef;
3570
+ let catchFallbackInternalWires = [];
3571
+ const catchAlt = sub(scopeLine, "scopeCatchAlt");
3572
+ if (catchAlt) {
3573
+ const preLen = wires.length;
3574
+ const altResult = extractCoalesceAlt(catchAlt, scopeLineNum);
3575
+ if ("literal" in altResult)
3576
+ catchFallback = altResult.literal;
3577
+ else if ("control" in altResult)
3578
+ catchControl = altResult.control;
3579
+ else {
3580
+ catchFallbackRef = altResult.sourceRef;
3581
+ catchFallbackInternalWires = wires.splice(preLen);
3582
+ }
3583
+ }
3584
+ wires.push({
3585
+ cond: condRef,
3586
+ ...(thenBranch.kind === "ref"
3587
+ ? { thenRef: thenBranch.ref }
3588
+ : { thenValue: thenBranch.value }),
3589
+ ...(elseBranch.kind === "ref"
3590
+ ? { elseRef: elseBranch.ref }
3591
+ : { elseValue: elseBranch.value }),
3592
+ ...(nullAltRefs.length > 0
3593
+ ? { falsyFallbackRefs: nullAltRefs }
3594
+ : {}),
3595
+ ...(falsyFallback !== undefined ? { falsyFallback } : {}),
3596
+ ...(falsyControl ? { falsyControl } : {}),
3597
+ ...(nullishFallback !== undefined ? { nullishFallback } : {}),
3598
+ ...(nullishFallbackRef !== undefined ? { nullishFallbackRef } : {}),
3599
+ ...(nullishControl ? { nullishControl } : {}),
3600
+ ...(catchFallback !== undefined ? { catchFallback } : {}),
3601
+ ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}),
3602
+ ...(catchControl ? { catchControl } : {}),
3603
+ to: toRef,
3604
+ });
3605
+ wires.push(...nullishFallbackInternalWires);
3606
+ wires.push(...catchFallbackInternalWires);
3607
+ continue;
3608
+ }
3609
+ sourceParts.push({ ref: condRef, isPipeFork: condIsPipeFork });
3610
+ let falsyFallback;
3611
+ let falsyControl;
3612
+ for (const alt of subs(scopeLine, "scopeNullAlt")) {
3613
+ const altResult = extractCoalesceAlt(alt, scopeLineNum);
3614
+ if ("literal" in altResult)
3615
+ falsyFallback = altResult.literal;
3616
+ else if ("control" in altResult)
3617
+ falsyControl = altResult.control;
3618
+ else
3619
+ sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false });
3620
+ }
3621
+ let nullishFallback;
3622
+ let nullishControl;
3623
+ let nullishFallbackRef;
3624
+ let nullishFallbackInternalWires = [];
3625
+ const nullishAlt = sub(scopeLine, "scopeNullishAlt");
3626
+ if (nullishAlt) {
3627
+ const preLen = wires.length;
3628
+ const altResult = extractCoalesceAlt(nullishAlt, scopeLineNum);
3629
+ if ("literal" in altResult)
3630
+ nullishFallback = altResult.literal;
3631
+ else if ("control" in altResult)
3632
+ nullishControl = altResult.control;
3633
+ else {
3634
+ nullishFallbackRef = altResult.sourceRef;
3635
+ nullishFallbackInternalWires = wires.splice(preLen);
3636
+ }
3637
+ }
3638
+ let catchFallback;
3639
+ let catchControl;
3640
+ let catchFallbackRef;
3641
+ let catchFallbackInternalWires = [];
3642
+ const catchAlt = sub(scopeLine, "scopeCatchAlt");
3643
+ if (catchAlt) {
3644
+ const preLen = wires.length;
3645
+ const altResult = extractCoalesceAlt(catchAlt, scopeLineNum);
3646
+ if ("literal" in altResult)
3647
+ catchFallback = altResult.literal;
3648
+ else if ("control" in altResult)
3649
+ catchControl = altResult.control;
3650
+ else {
3651
+ catchFallbackRef = altResult.sourceRef;
3652
+ catchFallbackInternalWires = wires.splice(preLen);
3653
+ }
3654
+ }
3655
+ const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0];
3656
+ const fallbackRefs = sourceParts.length > 1
3657
+ ? sourceParts.slice(1).map((p) => p.ref)
3658
+ : undefined;
3659
+ const wireAttrs = {
3660
+ ...(isPipe ? { pipe: true } : {}),
3661
+ ...(fallbackRefs ? { falsyFallbackRefs: fallbackRefs } : {}),
3662
+ ...(falsyFallback ? { falsyFallback } : {}),
3663
+ ...(falsyControl ? { falsyControl } : {}),
3664
+ ...(nullishFallback ? { nullishFallback } : {}),
3665
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
3666
+ ...(nullishControl ? { nullishControl } : {}),
3667
+ ...(catchFallback ? { catchFallback } : {}),
3668
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
3669
+ ...(catchControl ? { catchControl } : {}),
3670
+ };
3671
+ wires.push({ from: fromRef, to: toRef, ...wireAttrs });
3672
+ wires.push(...nullishFallbackInternalWires);
3673
+ wires.push(...catchFallbackInternalWires);
3674
+ }
3675
+ }
3676
+ }
3677
+ // ── Step 1.5: Process top-level node alias declarations ────────────────
3678
+ // `with <sourceExpr> as <alias>` at bridge body level (pipe-based).
3679
+ // Also detect simple renames via bridgeWithDecl when the root is already
3680
+ // a declared handle (e.g. `with api.some.complex.field as alias`).
3681
+ for (const bodyLine of bodyLines) {
3682
+ const c = bodyLine.children;
3683
+ // Handle pipe-based node aliases: with uc:i.category as upper
3684
+ const nodeAliasNode = c.bridgeNodeAlias?.[0];
3685
+ if (nodeAliasNode) {
3686
+ const lineNum = line(findFirstToken(nodeAliasNode));
3687
+ const alias = extractNameToken(sub(nodeAliasNode, "nodeAliasName"));
3688
+ assertNotReserved(alias, lineNum, "node alias");
3689
+ if (handleRes.has(alias)) {
3690
+ throw new Error(`Line ${lineNum}: Duplicate handle name "${alias}"`);
3691
+ }
3692
+ // ── Extract coalesce modifiers FIRST (shared by ternary + pull paths) ──
3693
+ let aliasFalsyFallback;
3694
+ let aliasFalsyControl;
3695
+ const aliasNullAltRefs = [];
3696
+ for (const alt of subs(nodeAliasNode, "aliasNullAlt")) {
3697
+ const altResult = extractCoalesceAlt(alt, lineNum);
3698
+ if ("literal" in altResult) {
3699
+ aliasFalsyFallback = altResult.literal;
3700
+ }
3701
+ else if ("control" in altResult) {
3702
+ aliasFalsyControl = altResult.control;
3703
+ }
3704
+ else {
3705
+ aliasNullAltRefs.push(altResult.sourceRef);
3706
+ }
3707
+ }
3708
+ let aliasNullishFallback;
3709
+ let aliasNullishControl;
3710
+ let aliasNullishFallbackRef;
3711
+ let aliasNullishFallbackInternalWires = [];
3712
+ const aliasNullishAlt = sub(nodeAliasNode, "aliasNullishAlt");
3713
+ if (aliasNullishAlt) {
3714
+ const preLen = wires.length;
3715
+ const altResult = extractCoalesceAlt(aliasNullishAlt, lineNum);
3716
+ if ("literal" in altResult) {
3717
+ aliasNullishFallback = altResult.literal;
3718
+ }
3719
+ else if ("control" in altResult) {
3720
+ aliasNullishControl = altResult.control;
3721
+ }
3722
+ else {
3723
+ aliasNullishFallbackRef = altResult.sourceRef;
3724
+ aliasNullishFallbackInternalWires = wires.splice(preLen);
3725
+ }
3726
+ }
3727
+ let aliasCatchFallback;
3728
+ let aliasCatchControl;
3729
+ let aliasCatchFallbackRef;
3730
+ let aliasCatchFallbackInternalWires = [];
3731
+ const aliasCatchAlt = sub(nodeAliasNode, "aliasCatchAlt");
3732
+ if (aliasCatchAlt) {
3733
+ const preLen = wires.length;
3734
+ const altResult = extractCoalesceAlt(aliasCatchAlt, lineNum);
3735
+ if ("literal" in altResult) {
3736
+ aliasCatchFallback = altResult.literal;
3737
+ }
3738
+ else if ("control" in altResult) {
3739
+ aliasCatchControl = altResult.control;
3740
+ }
3741
+ else {
3742
+ aliasCatchFallbackRef = altResult.sourceRef;
3743
+ aliasCatchFallbackInternalWires = wires.splice(preLen);
3744
+ }
3745
+ }
3746
+ const modifierAttrs = {
3747
+ ...(aliasNullAltRefs.length > 0
3748
+ ? { falsyFallbackRefs: aliasNullAltRefs }
3749
+ : {}),
3750
+ ...(aliasFalsyFallback ? { falsyFallback: aliasFalsyFallback } : {}),
3751
+ ...(aliasFalsyControl ? { falsyControl: aliasFalsyControl } : {}),
3752
+ ...(aliasNullishFallback
3753
+ ? { nullishFallback: aliasNullishFallback }
3754
+ : {}),
3755
+ ...(aliasNullishFallbackRef
3756
+ ? { nullishFallbackRef: aliasNullishFallbackRef }
3757
+ : {}),
3758
+ ...(aliasNullishControl ? { nullishControl: aliasNullishControl } : {}),
3759
+ ...(aliasCatchFallback ? { catchFallback: aliasCatchFallback } : {}),
3760
+ ...(aliasCatchFallbackRef
3761
+ ? { catchFallbackRef: aliasCatchFallbackRef }
3762
+ : {}),
3763
+ ...(aliasCatchControl ? { catchControl: aliasCatchControl } : {}),
3764
+ };
3765
+ // ── Compute the source ref ──
3766
+ let sourceRef;
3767
+ let aliasSafe;
3768
+ const aliasStringToken = nodeAliasNode.children.aliasStringSource?.[0];
3769
+ if (aliasStringToken) {
3770
+ // String literal source: alias "template..." [op right]* as name
3771
+ const raw = aliasStringToken.image.slice(1, -1);
3772
+ const segs = parseTemplateString(raw);
3773
+ const stringExprOps = subs(nodeAliasNode, "aliasStringExprOp");
3774
+ // Produce a NodeRef for the string value (concat fork or template desugar)
3775
+ const strRef = segs
3776
+ ? desugarTemplateString(segs, lineNum)
3777
+ : desugarTemplateString([{ kind: "text", value: raw }], lineNum);
3778
+ if (stringExprOps.length > 0) {
3779
+ const stringExprRights = subs(nodeAliasNode, "aliasStringExprRight");
3780
+ sourceRef = desugarExprChain(strRef, stringExprOps, stringExprRights, lineNum);
3781
+ }
3782
+ else {
3783
+ sourceRef = strRef;
3784
+ }
3785
+ // Ternary after string source (e.g. alias "a" == "b" ? x : y as name)
3786
+ const strTernaryOp = nodeAliasNode.children.aliasStringTernaryOp?.[0];
3787
+ if (strTernaryOp) {
3788
+ const thenNode = sub(nodeAliasNode, "aliasStringThenBranch");
3789
+ const elseNode = sub(nodeAliasNode, "aliasStringElseBranch");
3790
+ const thenBranch = extractTernaryBranch(thenNode, lineNum);
3791
+ const elseBranch = extractTernaryBranch(elseNode, lineNum);
3792
+ const ternaryToRef = {
3793
+ module: "__local",
3794
+ type: "Shadow",
3795
+ field: alias,
3796
+ path: [],
3797
+ };
3798
+ handleRes.set(alias, {
3799
+ module: "__local",
3800
+ type: "Shadow",
3801
+ field: alias,
3802
+ });
3803
+ wires.push({
3804
+ cond: sourceRef,
3805
+ ...(thenBranch.kind === "ref"
3806
+ ? { thenRef: thenBranch.ref }
3807
+ : { thenValue: thenBranch.value }),
3808
+ ...(elseBranch.kind === "ref"
3809
+ ? { elseRef: elseBranch.ref }
3810
+ : { elseValue: elseBranch.value }),
3811
+ ...modifierAttrs,
3812
+ to: ternaryToRef,
3813
+ });
3814
+ wires.push(...aliasNullishFallbackInternalWires);
3815
+ wires.push(...aliasCatchFallbackInternalWires);
3816
+ continue;
3817
+ }
3818
+ aliasSafe = false;
3819
+ }
3820
+ else {
3821
+ // Normal expression source
3822
+ const firstParenNode = sub(nodeAliasNode, "aliasFirstParen");
3823
+ const firstSourceNode = sub(nodeAliasNode, "nodeAliasSource");
3824
+ const headNode = firstSourceNode
3825
+ ? sub(firstSourceNode, "head")
3826
+ : undefined;
3827
+ const isSafe = headNode
3828
+ ? !!extractAddressPath(headNode).rootSafe
3829
+ : false;
3830
+ const exprOps = subs(nodeAliasNode, "aliasExprOp");
3831
+ let condRef;
3832
+ if (firstParenNode) {
3833
+ const parenRef = resolveParenExpr(firstParenNode, lineNum, undefined, isSafe);
3834
+ if (exprOps.length > 0) {
3835
+ const exprRights = subs(nodeAliasNode, "aliasExprRight");
3836
+ condRef = desugarExprChain(parenRef, exprOps, exprRights, lineNum, undefined, isSafe);
3837
+ }
3838
+ else {
3839
+ condRef = parenRef;
3840
+ }
3841
+ }
3842
+ else if (exprOps.length > 0) {
3843
+ const exprRights = subs(nodeAliasNode, "aliasExprRight");
3844
+ const leftRef = buildSourceExpr(firstSourceNode, lineNum);
3845
+ condRef = desugarExprChain(leftRef, exprOps, exprRights, lineNum, undefined, isSafe);
3846
+ }
3847
+ else {
3848
+ const result = buildSourceExprSafe(firstSourceNode, lineNum);
3849
+ condRef = result.ref;
3850
+ aliasSafe = result.safe;
3851
+ }
3852
+ // Apply `not` prefix if present
3853
+ if (nodeAliasNode.children.aliasNotPrefix?.[0]) {
3854
+ condRef = desugarNot(condRef, lineNum, isSafe);
3855
+ }
3856
+ // Ternary
3857
+ const ternaryOp = nodeAliasNode.children.aliasTernaryOp?.[0];
3858
+ if (ternaryOp) {
3859
+ const thenNode = sub(nodeAliasNode, "aliasThenBranch");
3860
+ const elseNode = sub(nodeAliasNode, "aliasElseBranch");
3861
+ const thenBranch = extractTernaryBranch(thenNode, lineNum);
3862
+ const elseBranch = extractTernaryBranch(elseNode, lineNum);
3863
+ const ternaryToRef = {
3864
+ module: "__local",
3865
+ type: "Shadow",
3866
+ field: alias,
3867
+ path: [],
3868
+ };
3869
+ handleRes.set(alias, {
3870
+ module: "__local",
3871
+ type: "Shadow",
3872
+ field: alias,
3873
+ });
3874
+ wires.push({
3875
+ cond: condRef,
3876
+ ...(thenBranch.kind === "ref"
3877
+ ? { thenRef: thenBranch.ref }
3878
+ : { thenValue: thenBranch.value }),
3879
+ ...(elseBranch.kind === "ref"
3880
+ ? { elseRef: elseBranch.ref }
3881
+ : { elseValue: elseBranch.value }),
3882
+ ...modifierAttrs,
3883
+ to: ternaryToRef,
3884
+ });
3885
+ wires.push(...aliasNullishFallbackInternalWires);
3886
+ wires.push(...aliasCatchFallbackInternalWires);
3887
+ continue;
3888
+ }
3889
+ sourceRef = condRef;
3890
+ if (aliasSafe === undefined)
3891
+ aliasSafe = isSafe || undefined;
3892
+ }
3893
+ // Create __local trunk for the alias
3894
+ const localRes = {
3895
+ module: "__local",
3896
+ type: "Shadow",
3897
+ field: alias,
3898
+ };
3899
+ handleRes.set(alias, localRes);
3900
+ // Emit wire from source to local trunk
3901
+ const localToRef = {
3902
+ module: "__local",
3903
+ type: "Shadow",
3904
+ field: alias,
3905
+ path: [],
3906
+ };
3907
+ const aliasAttrs = {
3908
+ ...(aliasSafe ? { safe: true } : {}),
3909
+ ...modifierAttrs,
3910
+ };
3911
+ wires.push({ from: sourceRef, to: localToRef, ...aliasAttrs });
3912
+ wires.push(...aliasNullishFallbackInternalWires);
3913
+ wires.push(...aliasCatchFallbackInternalWires);
3914
+ }
3915
+ }
3916
+ // ── Step 2: Process wire lines ─────────────────────────────────────────
3917
+ for (const bodyLine of bodyLines) {
3918
+ const c = bodyLine.children;
3919
+ if (c.bridgeWithDecl)
3920
+ continue; // already processed
3921
+ if (c.bridgeNodeAlias)
3922
+ continue; // already processed in Step 1.5
3923
+ if (c.bridgeForce)
3924
+ continue; // handled below
3925
+ const wireNode = c.bridgeWire?.[0];
3926
+ if (!wireNode)
3927
+ continue;
3928
+ const wc = wireNode.children;
3929
+ const lineNum = line(findFirstToken(wireNode));
3930
+ // Parse target
3931
+ const { root: targetRoot, segments: targetSegs } = extractAddressPath(sub(wireNode, "target"));
3932
+ const toRef = resolveAddress(targetRoot, targetSegs, lineNum);
3933
+ assertNoTargetIndices(toRef, lineNum);
3934
+ // ── Constant wire: target = value ──
3935
+ if (wc.equalsOp) {
3936
+ const value = extractBareValue(sub(wireNode, "constValue"));
3937
+ wires.push({ value, to: toRef });
3938
+ continue;
3939
+ }
3940
+ // ── Path scoping block: target { .field ... } ──
3941
+ if (wc.scopeBlock) {
3942
+ // Process alias declarations inside the scope block first
3943
+ const scopeAliases = subs(wireNode, "scopeAlias");
3944
+ for (const aliasNode of scopeAliases) {
3945
+ const aliasLineNum = line(findFirstToken(aliasNode));
3946
+ const sourceNode = sub(aliasNode, "nodeAliasSource");
3947
+ const alias = extractNameToken(sub(aliasNode, "nodeAliasName"));
3948
+ assertNotReserved(alias, aliasLineNum, "node alias");
3949
+ if (handleRes.has(alias)) {
3950
+ throw new Error(`Line ${aliasLineNum}: Duplicate handle name "${alias}"`);
3951
+ }
3952
+ const { ref: sourceRef, safe: aliasSafe } = buildSourceExprSafe(sourceNode, aliasLineNum);
3953
+ const localRes = {
3954
+ module: "__local",
3955
+ type: "Shadow",
3956
+ field: alias,
3957
+ };
3958
+ handleRes.set(alias, localRes);
3959
+ const localToRef = {
3960
+ module: "__local",
3961
+ type: "Shadow",
3962
+ field: alias,
3963
+ path: [],
3964
+ };
3965
+ wires.push({
3966
+ from: sourceRef,
3967
+ to: localToRef,
3968
+ ...(aliasSafe ? { safe: true } : {}),
3969
+ });
3970
+ }
3971
+ const scopeLines = subs(wireNode, "pathScopeLine");
3972
+ processScopeLines(scopeLines, targetRoot, targetSegs);
3973
+ continue;
3974
+ }
3975
+ // ── Pull wire: target <- source [modifiers] ──
3976
+ // ── String source (template or plain): target <- "..." ──
3977
+ const stringSourceToken = wc.stringSource?.[0];
3978
+ if (stringSourceToken) {
3979
+ const raw = stringSourceToken.image.slice(1, -1); // strip quotes
3980
+ const segs = parseTemplateString(raw);
3981
+ // Process coalesce modifiers
3982
+ let falsyFallback;
3983
+ let falsyControl;
3984
+ const nullAltRefs = [];
3985
+ for (const alt of subs(wireNode, "nullAlt")) {
3986
+ const altResult = extractCoalesceAlt(alt, lineNum);
3987
+ if ("literal" in altResult) {
3988
+ falsyFallback = altResult.literal;
3989
+ }
3990
+ else if ("control" in altResult) {
3991
+ falsyControl = altResult.control;
3992
+ }
3993
+ else {
3994
+ nullAltRefs.push(altResult.sourceRef);
3995
+ }
3996
+ }
3997
+ let nullishFallback;
3998
+ let nullishControl;
3999
+ let nullishFallbackRef;
4000
+ let nullishFallbackInternalWires = [];
4001
+ const nullishAlt = sub(wireNode, "nullishAlt");
4002
+ if (nullishAlt) {
4003
+ const preLen = wires.length;
4004
+ const altResult = extractCoalesceAlt(nullishAlt, lineNum);
4005
+ if ("literal" in altResult) {
4006
+ nullishFallback = altResult.literal;
4007
+ }
4008
+ else if ("control" in altResult) {
4009
+ nullishControl = altResult.control;
4010
+ }
4011
+ else {
4012
+ nullishFallbackRef = altResult.sourceRef;
4013
+ nullishFallbackInternalWires = wires.splice(preLen);
4014
+ }
4015
+ }
4016
+ let catchFallback;
4017
+ let catchControl;
4018
+ let catchFallbackRef;
4019
+ let catchFallbackInternalWires = [];
4020
+ const catchAlt = sub(wireNode, "catchAlt");
4021
+ if (catchAlt) {
4022
+ const preLen = wires.length;
4023
+ const altResult = extractCoalesceAlt(catchAlt, lineNum);
4024
+ if ("literal" in altResult) {
4025
+ catchFallback = altResult.literal;
4026
+ }
4027
+ else if ("control" in altResult) {
4028
+ catchControl = altResult.control;
4029
+ }
4030
+ else {
4031
+ catchFallbackRef = altResult.sourceRef;
4032
+ catchFallbackInternalWires = wires.splice(preLen);
4033
+ }
4034
+ }
4035
+ const lastAttrs = {
4036
+ ...(falsyFallback ? { falsyFallback } : {}),
4037
+ ...(falsyControl ? { falsyControl } : {}),
4038
+ ...(nullishFallback ? { nullishFallback } : {}),
4039
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
4040
+ ...(nullishControl ? { nullishControl } : {}),
4041
+ ...(catchFallback ? { catchFallback } : {}),
4042
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
4043
+ ...(catchControl ? { catchControl } : {}),
4044
+ };
4045
+ if (segs) {
4046
+ // Template string — desugar to synthetic internal.concat fork
4047
+ const concatOutRef = desugarTemplateString(segs, lineNum);
4048
+ wires.push({ from: concatOutRef, to: toRef, pipe: true, ...lastAttrs });
4049
+ }
4050
+ else {
4051
+ // Plain string without interpolation — emit constant wire
4052
+ wires.push({ value: raw, to: toRef, ...lastAttrs });
4053
+ }
4054
+ for (const ref of nullAltRefs) {
4055
+ wires.push({ from: ref, to: toRef });
4056
+ }
4057
+ wires.push(...nullishFallbackInternalWires);
4058
+ wires.push(...catchFallbackInternalWires);
4059
+ continue;
4060
+ }
4061
+ // Array mapping?
4062
+ const arrayMappingNode = wc.arrayMapping?.[0];
4063
+ if (arrayMappingNode) {
4064
+ const firstSourceNode = sub(wireNode, "firstSource");
4065
+ const firstParenNode = sub(wireNode, "firstParenExpr");
4066
+ const srcRef = firstParenNode
4067
+ ? resolveParenExpr(firstParenNode, lineNum)
4068
+ : buildSourceExpr(firstSourceNode, lineNum);
4069
+ // Process coalesce modifiers on the array wire (same as plain pull wires)
4070
+ let arrayFalsyFallback;
4071
+ let arrayFalsyControl;
4072
+ const arrayNullAltRefs = [];
4073
+ for (const alt of subs(wireNode, "nullAlt")) {
4074
+ const altResult = extractCoalesceAlt(alt, lineNum);
4075
+ if ("literal" in altResult) {
4076
+ arrayFalsyFallback = altResult.literal;
4077
+ }
4078
+ else if ("control" in altResult) {
4079
+ arrayFalsyControl = altResult.control;
4080
+ }
4081
+ else {
4082
+ arrayNullAltRefs.push(altResult.sourceRef);
4083
+ }
4084
+ }
4085
+ let arrayNullishFallback;
4086
+ let arrayNullishControl;
4087
+ let arrayNullishFallbackRef;
4088
+ let arrayNullishFallbackInternalWires = [];
4089
+ const arrayNullishAlt = sub(wireNode, "nullishAlt");
4090
+ if (arrayNullishAlt) {
4091
+ const preLen = wires.length;
4092
+ const altResult = extractCoalesceAlt(arrayNullishAlt, lineNum);
4093
+ if ("literal" in altResult) {
4094
+ arrayNullishFallback = altResult.literal;
4095
+ }
4096
+ else if ("control" in altResult) {
4097
+ arrayNullishControl = altResult.control;
4098
+ }
4099
+ else {
4100
+ arrayNullishFallbackRef = altResult.sourceRef;
4101
+ arrayNullishFallbackInternalWires = wires.splice(preLen);
4102
+ }
4103
+ }
4104
+ let arrayCatchFallback;
4105
+ let arrayCatchControl;
4106
+ let arrayCatchFallbackRef;
4107
+ let arrayCatchFallbackInternalWires = [];
4108
+ const arrayCatchAlt = sub(wireNode, "catchAlt");
4109
+ if (arrayCatchAlt) {
4110
+ const preLen = wires.length;
4111
+ const altResult = extractCoalesceAlt(arrayCatchAlt, lineNum);
4112
+ if ("literal" in altResult) {
4113
+ arrayCatchFallback = altResult.literal;
4114
+ }
4115
+ else if ("control" in altResult) {
4116
+ arrayCatchControl = altResult.control;
4117
+ }
4118
+ else {
4119
+ arrayCatchFallbackRef = altResult.sourceRef;
4120
+ arrayCatchFallbackInternalWires = wires.splice(preLen);
4121
+ }
4122
+ }
4123
+ const arrayWireAttrs = {
4124
+ ...(arrayFalsyFallback ? { falsyFallback: arrayFalsyFallback } : {}),
4125
+ ...(arrayFalsyControl ? { falsyControl: arrayFalsyControl } : {}),
4126
+ ...(arrayNullishFallback
4127
+ ? { nullishFallback: arrayNullishFallback }
4128
+ : {}),
4129
+ ...(arrayNullishFallbackRef
4130
+ ? { nullishFallbackRef: arrayNullishFallbackRef }
4131
+ : {}),
4132
+ ...(arrayNullishControl ? { nullishControl: arrayNullishControl } : {}),
4133
+ ...(arrayCatchFallback ? { catchFallback: arrayCatchFallback } : {}),
4134
+ ...(arrayCatchFallbackRef
4135
+ ? { catchFallbackRef: arrayCatchFallbackRef }
4136
+ : {}),
4137
+ ...(arrayCatchControl ? { catchControl: arrayCatchControl } : {}),
4138
+ };
4139
+ wires.push({ from: srcRef, to: toRef, ...arrayWireAttrs });
4140
+ for (const ref of arrayNullAltRefs) {
4141
+ wires.push({ from: ref, to: toRef });
4142
+ }
4143
+ wires.push(...arrayNullishFallbackInternalWires);
4144
+ wires.push(...arrayCatchFallbackInternalWires);
4145
+ const iterName = extractNameToken(sub(arrayMappingNode, "iterName"));
4146
+ assertNotReserved(iterName, lineNum, "iterator handle");
4147
+ const arrayToPath = toRef.path;
4148
+ arrayIterators[arrayToPath.join(".")] = iterName;
4149
+ // Process element lines (supports nested array mappings recursively)
4150
+ const elemWithDecls = subs(arrayMappingNode, "elementWithDecl");
4151
+ const cleanup = processLocalBindings(elemWithDecls, iterName);
4152
+ processElementLines(subs(arrayMappingNode, "elementLine"), arrayToPath, iterName, bridgeType, bridgeField, wires, arrayIterators, buildSourceExpr, extractCoalesceAlt, desugarExprChain, extractTernaryBranch, processLocalBindings, desugarTemplateString, desugarNot, resolveParenExpr);
4153
+ cleanup();
4154
+ continue;
4155
+ }
4156
+ const firstSourceNode = sub(wireNode, "firstSource");
4157
+ const firstParenNode = sub(wireNode, "firstParenExpr");
4158
+ const sourceParts = [];
4159
+ // Check for safe navigation (?.) on the head address path
4160
+ const headNode = firstSourceNode ? sub(firstSourceNode, "head") : undefined;
4161
+ const isSafe = headNode ? !!extractAddressPath(headNode).rootSafe : false;
4162
+ const exprOps = subs(wireNode, "exprOp");
4163
+ // Compute condition ref (expression chain result or plain source)
4164
+ let condRef;
4165
+ let condIsPipeFork;
4166
+ if (firstParenNode) {
4167
+ // First source is a parenthesized sub-expression
4168
+ const parenRef = resolveParenExpr(firstParenNode, lineNum, undefined, isSafe);
4169
+ if (exprOps.length > 0) {
4170
+ const exprRights = subs(wireNode, "exprRight");
4171
+ condRef = desugarExprChain(parenRef, exprOps, exprRights, lineNum, undefined, isSafe);
4172
+ }
4173
+ else {
4174
+ condRef = parenRef;
4175
+ }
4176
+ condIsPipeFork = true;
4177
+ }
4178
+ else if (exprOps.length > 0) {
4179
+ // It's a math/comparison expression — desugar it.
4180
+ const exprRights = subs(wireNode, "exprRight");
4181
+ const leftRef = buildSourceExpr(firstSourceNode, lineNum);
4182
+ condRef = desugarExprChain(leftRef, exprOps, exprRights, lineNum, undefined, isSafe);
4183
+ condIsPipeFork = true;
4184
+ }
4185
+ else {
4186
+ const pipeSegs = subs(firstSourceNode, "pipeSegment");
4187
+ condRef = buildSourceExpr(firstSourceNode, lineNum);
4188
+ condIsPipeFork =
4189
+ condRef.instance != null &&
4190
+ condRef.path.length === 0 &&
4191
+ pipeSegs.length > 0;
4192
+ }
4193
+ // ── Apply `not` prefix if present ──
4194
+ if (wc.notPrefix) {
4195
+ condRef = desugarNot(condRef, lineNum, isSafe);
4196
+ condIsPipeFork = true;
4197
+ }
4198
+ // ── Ternary wire: cond ? thenBranch : elseBranch ──
4199
+ const ternaryOp = tok(wireNode, "ternaryOp");
4200
+ if (ternaryOp) {
4201
+ const thenNode = sub(wireNode, "thenBranch");
4202
+ const elseNode = sub(wireNode, "elseBranch");
4203
+ const thenBranch = extractTernaryBranch(thenNode, lineNum);
4204
+ const elseBranch = extractTernaryBranch(elseNode, lineNum);
4205
+ // Process || null-coalesce alternatives.
4206
+ // Literals → stored on the ternary wire; source refs → sibling pull wires.
4207
+ let falsyFallback;
4208
+ let falsyControl;
4209
+ const nullAltRefs = [];
4210
+ for (const alt of subs(wireNode, "nullAlt")) {
4211
+ const altResult = extractCoalesceAlt(alt, lineNum);
4212
+ if ("literal" in altResult) {
4213
+ falsyFallback = altResult.literal;
4214
+ }
4215
+ else if ("control" in altResult) {
4216
+ falsyControl = altResult.control;
4217
+ }
4218
+ else {
4219
+ nullAltRefs.push(altResult.sourceRef);
4220
+ }
4221
+ }
4222
+ // Process ?? nullish fallback.
4223
+ let nullishFallback;
4224
+ let nullishControl;
4225
+ let nullishFallbackRef;
4226
+ let nullishFallbackInternalWires = [];
4227
+ const nullishAlt = sub(wireNode, "nullishAlt");
4228
+ if (nullishAlt) {
4229
+ const preLen = wires.length;
4230
+ const altResult = extractCoalesceAlt(nullishAlt, lineNum);
4231
+ if ("literal" in altResult) {
4232
+ nullishFallback = altResult.literal;
4233
+ }
4234
+ else if ("control" in altResult) {
4235
+ nullishControl = altResult.control;
4236
+ }
4237
+ else {
4238
+ nullishFallbackRef = altResult.sourceRef;
4239
+ nullishFallbackInternalWires = wires.splice(preLen);
4240
+ }
4241
+ }
4242
+ // Process catch error fallback.
4243
+ let catchFallback;
4244
+ let catchControl;
4245
+ let catchFallbackRef;
4246
+ let catchFallbackInternalWires = [];
4247
+ const catchAlt = sub(wireNode, "catchAlt");
4248
+ if (catchAlt) {
4249
+ const preLen = wires.length;
4250
+ const altResult = extractCoalesceAlt(catchAlt, lineNum);
4251
+ if ("literal" in altResult) {
4252
+ catchFallback = altResult.literal;
4253
+ }
4254
+ else if ("control" in altResult) {
4255
+ catchControl = altResult.control;
4256
+ }
4257
+ else {
4258
+ catchFallbackRef = altResult.sourceRef;
4259
+ catchFallbackInternalWires = wires.splice(preLen);
4260
+ }
4261
+ }
4262
+ wires.push({
4263
+ cond: condRef,
4264
+ ...(thenBranch.kind === "ref"
4265
+ ? { thenRef: thenBranch.ref }
4266
+ : { thenValue: thenBranch.value }),
4267
+ ...(elseBranch.kind === "ref"
4268
+ ? { elseRef: elseBranch.ref }
4269
+ : { elseValue: elseBranch.value }),
4270
+ ...(nullAltRefs.length > 0 ? { falsyFallbackRefs: nullAltRefs } : {}),
4271
+ ...(falsyFallback !== undefined ? { falsyFallback } : {}),
4272
+ ...(falsyControl ? { falsyControl } : {}),
4273
+ ...(nullishFallback !== undefined ? { nullishFallback } : {}),
4274
+ ...(nullishFallbackRef !== undefined ? { nullishFallbackRef } : {}),
4275
+ ...(nullishControl ? { nullishControl } : {}),
4276
+ ...(catchFallback !== undefined ? { catchFallback } : {}),
4277
+ ...(catchFallbackRef !== undefined ? { catchFallbackRef } : {}),
4278
+ ...(catchControl ? { catchControl } : {}),
4279
+ to: toRef,
4280
+ });
4281
+ wires.push(...nullishFallbackInternalWires);
4282
+ wires.push(...catchFallbackInternalWires);
4283
+ continue;
4284
+ }
4285
+ sourceParts.push({ ref: condRef, isPipeFork: condIsPipeFork });
4286
+ let falsyFallback;
4287
+ let falsyControl;
4288
+ for (const alt of subs(wireNode, "nullAlt")) {
4289
+ const altResult = extractCoalesceAlt(alt, lineNum);
4290
+ if ("literal" in altResult) {
4291
+ falsyFallback = altResult.literal;
4292
+ }
4293
+ else if ("control" in altResult) {
4294
+ falsyControl = altResult.control;
4295
+ }
4296
+ else {
4297
+ sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false });
4298
+ }
4299
+ }
4300
+ let nullishFallback;
4301
+ let nullishControl;
4302
+ let nullishFallbackRef;
4303
+ let nullishFallbackInternalWires = [];
4304
+ const nullishAlt = sub(wireNode, "nullishAlt");
4305
+ if (nullishAlt) {
4306
+ const preLen = wires.length;
4307
+ const altResult = extractCoalesceAlt(nullishAlt, lineNum);
4308
+ if ("literal" in altResult) {
4309
+ nullishFallback = altResult.literal;
4310
+ }
4311
+ else if ("control" in altResult) {
4312
+ nullishControl = altResult.control;
4313
+ }
4314
+ else {
4315
+ nullishFallbackRef = altResult.sourceRef;
4316
+ nullishFallbackInternalWires = wires.splice(preLen);
4317
+ }
4318
+ }
4319
+ let catchFallback;
4320
+ let catchControl;
4321
+ let catchFallbackRef;
4322
+ let catchFallbackInternalWires = [];
4323
+ const catchAlt = sub(wireNode, "catchAlt");
4324
+ if (catchAlt) {
4325
+ const preLen = wires.length;
4326
+ const altResult = extractCoalesceAlt(catchAlt, lineNum);
4327
+ if ("literal" in altResult) {
4328
+ catchFallback = altResult.literal;
4329
+ }
4330
+ else if ("control" in altResult) {
4331
+ catchControl = altResult.control;
4332
+ }
4333
+ else {
4334
+ catchFallbackRef = altResult.sourceRef;
4335
+ catchFallbackInternalWires = wires.splice(preLen);
4336
+ }
4337
+ }
4338
+ const { ref: fromRef, isPipeFork: isPipe } = sourceParts[0];
4339
+ const fallbackRefs = sourceParts.length > 1
4340
+ ? sourceParts.slice(1).map((p) => p.ref)
4341
+ : undefined;
4342
+ const wireAttrs = {
4343
+ ...(isSafe ? { safe: true } : {}),
4344
+ ...(isPipe ? { pipe: true } : {}),
4345
+ ...(fallbackRefs ? { falsyFallbackRefs: fallbackRefs } : {}),
4346
+ ...(falsyFallback ? { falsyFallback } : {}),
4347
+ ...(falsyControl ? { falsyControl } : {}),
4348
+ ...(nullishFallback ? { nullishFallback } : {}),
4349
+ ...(nullishFallbackRef ? { nullishFallbackRef } : {}),
4350
+ ...(nullishControl ? { nullishControl } : {}),
4351
+ ...(catchFallback ? { catchFallback } : {}),
4352
+ ...(catchFallbackRef ? { catchFallbackRef } : {}),
4353
+ ...(catchControl ? { catchControl } : {}),
4354
+ };
4355
+ wires.push({ from: fromRef, to: toRef, ...wireAttrs });
4356
+ wires.push(...nullishFallbackInternalWires);
4357
+ wires.push(...catchFallbackInternalWires);
4358
+ }
4359
+ // ── Step 3: Collect force statements ──────────────────────────────────
4360
+ const forces = [];
4361
+ for (const bodyLine of bodyLines) {
4362
+ const forceNode = bodyLine.children.bridgeForce?.[0];
4363
+ if (!forceNode)
4364
+ continue;
4365
+ const lineNum = line(findFirstToken(forceNode));
4366
+ const handle = extractNameToken(sub(forceNode, "forcedHandle"));
4367
+ const res = handleRes.get(handle);
4368
+ if (!res) {
4369
+ throw new Error(`Line ${lineNum}: Cannot force undeclared handle "${handle}". Add 'with ${handle}' to the bridge header.`);
4370
+ }
4371
+ const fc = forceNode.children;
4372
+ const catchError = !!fc.forceCatchKw?.length;
4373
+ forces.push({
4374
+ handle,
4375
+ ...res,
4376
+ ...(catchError ? { catchError: true } : {}),
4377
+ });
4378
+ }
4379
+ return {
4380
+ handles: handleBindings,
4381
+ wires,
4382
+ arrayIterators,
4383
+ pipeHandles: pipeHandleEntries,
4384
+ forces,
4385
+ };
4386
+ }
4387
+ // ═══════════════════════════════════════════════════════════════════════════
4388
+ // inlineDefine (matching the regex parser)
4389
+ // ═══════════════════════════════════════════════════════════════════════════
4390
+ function inlineDefine(defineHandle, defineDef, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef) {
4391
+ const genericModule = `__define_${defineHandle}`;
4392
+ const inModule = `__define_in_${defineHandle}`;
4393
+ const outModule = `__define_out_${defineHandle}`;
4394
+ const defType = "Define";
4395
+ const defField = defineDef.name;
4396
+ const defCounters = new Map();
4397
+ const trunkRemap = new Map();
4398
+ for (const hb of defineDef.handles) {
4399
+ if (hb.kind === "input" ||
4400
+ hb.kind === "output" ||
4401
+ hb.kind === "context" ||
4402
+ hb.kind === "const")
4403
+ continue;
4404
+ if (hb.kind === "define")
4405
+ continue;
4406
+ const name = hb.kind === "tool" ? hb.name : "";
4407
+ if (!name)
4408
+ continue;
4409
+ const lastDot = name.lastIndexOf(".");
4410
+ let oldModule, oldType, oldField, instanceKey, bridgeKey;
4411
+ if (lastDot !== -1) {
4412
+ oldModule = name.substring(0, lastDot);
4413
+ oldType = defType;
4414
+ oldField = name.substring(lastDot + 1);
4415
+ instanceKey = `${oldModule}:${oldField}`;
4416
+ bridgeKey = instanceKey;
4417
+ }
4418
+ else {
4419
+ oldModule = SELF_MODULE;
4420
+ oldType = "Tools";
4421
+ oldField = name;
4422
+ instanceKey = `Tools:${name}`;
4423
+ bridgeKey = instanceKey;
4424
+ }
4425
+ const oldInstance = (defCounters.get(instanceKey) ?? 0) + 1;
4426
+ defCounters.set(instanceKey, oldInstance);
4427
+ const newInstance = (instanceCounters.get(bridgeKey) ?? 0) + 1;
4428
+ instanceCounters.set(bridgeKey, newInstance);
4429
+ const oldKey = `${oldModule}:${oldType}:${oldField}:${oldInstance}`;
4430
+ trunkRemap.set(oldKey, {
4431
+ module: oldModule,
4432
+ type: oldType,
4433
+ field: oldField,
4434
+ instance: newInstance,
4435
+ });
4436
+ handleBindings.push({
4437
+ handle: `${defineHandle}$${hb.handle}`,
4438
+ kind: "tool",
4439
+ name,
4440
+ });
4441
+ }
4442
+ // Remap existing bridge wires pointing at the generic define module
4443
+ for (const wire of wires) {
4444
+ if ("from" in wire) {
4445
+ if (wire.to.module === genericModule)
4446
+ wire.to = { ...wire.to, module: inModule };
4447
+ if (wire.from.module === genericModule)
4448
+ wire.from = { ...wire.from, module: outModule };
4449
+ if (wire.nullishFallbackRef?.module === genericModule)
4450
+ wire.nullishFallbackRef = {
4451
+ ...wire.nullishFallbackRef,
4452
+ module: outModule,
4453
+ };
4454
+ if (wire.catchFallbackRef?.module === genericModule)
4455
+ wire.catchFallbackRef = { ...wire.catchFallbackRef, module: outModule };
4456
+ }
4457
+ if ("value" in wire && wire.to.module === genericModule)
4458
+ wire.to = { ...wire.to, module: inModule };
4459
+ }
4460
+ const forkOffset = nextForkSeqRef.value;
4461
+ let maxDefForkSeq = 0;
4462
+ function remapRef(ref, side) {
4463
+ if (ref.module === SELF_MODULE &&
4464
+ ref.type === defType &&
4465
+ ref.field === defField) {
4466
+ const targetModule = side === "from" ? inModule : outModule;
4467
+ return {
4468
+ ...ref,
4469
+ module: targetModule,
4470
+ type: bridgeType,
4471
+ field: bridgeField,
4472
+ };
4473
+ }
4474
+ const key = `${ref.module}:${ref.type}:${ref.field}:${ref.instance ?? ""}`;
4475
+ const newTrunk = trunkRemap.get(key);
4476
+ if (newTrunk)
4477
+ return {
4478
+ ...ref,
4479
+ module: newTrunk.module,
4480
+ type: newTrunk.type,
4481
+ field: newTrunk.field,
4482
+ instance: newTrunk.instance,
4483
+ };
4484
+ if (ref.instance != null && ref.instance >= 100000) {
4485
+ const defSeq = ref.instance - 100000;
4486
+ if (defSeq + 1 > maxDefForkSeq)
4487
+ maxDefForkSeq = defSeq + 1;
4488
+ return { ...ref, instance: ref.instance + forkOffset };
4489
+ }
4490
+ return ref;
4491
+ }
4492
+ for (const wire of defineDef.wires) {
4493
+ const cloned = JSON.parse(JSON.stringify(wire));
4494
+ if ("from" in cloned) {
4495
+ cloned.from = remapRef(cloned.from, "from");
4496
+ cloned.to = remapRef(cloned.to, "to");
4497
+ if (cloned.nullishFallbackRef)
4498
+ cloned.nullishFallbackRef = remapRef(cloned.nullishFallbackRef, "from");
4499
+ if (cloned.catchFallbackRef)
4500
+ cloned.catchFallbackRef = remapRef(cloned.catchFallbackRef, "from");
4501
+ }
4502
+ else {
4503
+ cloned.to = remapRef(cloned.to, "to");
4504
+ }
4505
+ wires.push(cloned);
4506
+ }
4507
+ nextForkSeqRef.value += maxDefForkSeq;
4508
+ if (defineDef.pipeHandles) {
4509
+ for (const ph of defineDef.pipeHandles) {
4510
+ const parts = ph.key.split(":");
4511
+ const phInstance = parseInt(parts[parts.length - 1]);
4512
+ let newKey = ph.key;
4513
+ if (phInstance >= 100000) {
4514
+ const newInst = phInstance + forkOffset;
4515
+ parts[parts.length - 1] = String(newInst);
4516
+ newKey = parts.join(":");
4517
+ }
4518
+ const bt = ph.baseTrunk;
4519
+ const btKey = `${bt.module}:${defType}:${bt.field}:${bt.instance ?? ""}`;
4520
+ const newBt = trunkRemap.get(btKey);
4521
+ const btKey2 = `${bt.module}:Tools:${bt.field}:${bt.instance ?? ""}`;
4522
+ const newBt2 = trunkRemap.get(btKey2);
4523
+ const resolvedBt = newBt ?? newBt2;
4524
+ pipeHandleEntries.push({
4525
+ key: newKey,
4526
+ handle: `${defineHandle}$${ph.handle}`,
4527
+ baseTrunk: resolvedBt
4528
+ ? {
4529
+ module: resolvedBt.module,
4530
+ type: resolvedBt.type,
4531
+ field: resolvedBt.field,
4532
+ instance: resolvedBt.instance,
4533
+ }
4534
+ : ph.baseTrunk,
4535
+ });
4536
+ }
4537
+ }
4538
+ }