@stackables/bridge-compiler 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/build/bridge-format.d.ts +8 -0
- package/build/bridge-format.d.ts.map +1 -0
- package/build/bridge-format.js +1334 -0
- package/build/bridge-lint.d.ts +3 -0
- package/build/bridge-lint.d.ts.map +1 -0
- package/build/bridge-lint.js +73 -0
- package/build/index.d.ts +13 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +13 -0
- package/build/language-service.d.ts +50 -0
- package/build/language-service.d.ts.map +1 -0
- package/build/language-service.js +243 -0
- package/build/parser/index.d.ts +9 -0
- package/build/parser/index.d.ts.map +1 -0
- package/build/parser/index.js +7 -0
- package/build/parser/lexer.d.ts +68 -0
- package/build/parser/lexer.d.ts.map +1 -0
- package/build/parser/lexer.js +160 -0
- package/build/parser/parser.d.ts +29 -0
- package/build/parser/parser.d.ts.map +1 -0
- package/build/parser/parser.js +4538 -0
- package/package.json +39 -0
|
@@ -0,0 +1,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
|
+
}
|