@stackables/bridge 1.6.1 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/ExecutionTree.js +1 -1
- package/build/bridge-format.d.ts +3 -10
- package/build/bridge-format.d.ts.map +1 -1
- package/build/bridge-format.js +22 -1264
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/parser/index.d.ts +8 -0
- package/build/parser/index.d.ts.map +1 -0
- package/build/parser/index.js +7 -0
- package/build/parser/lexer.d.ts +45 -0
- package/build/parser/lexer.d.ts.map +1 -0
- package/build/parser/lexer.js +114 -0
- package/build/parser/parser.d.ts +3 -0
- package/build/parser/parser.d.ts.map +1 -0
- package/build/parser/parser.js +1536 -0
- package/build/utils.d.ts +9 -0
- package/build/utils.d.ts.map +1 -0
- package/build/utils.js +23 -0
- package/package.json +2 -1
|
@@ -0,0 +1,1536 @@
|
|
|
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, ForceArrow, NullCoalesce, ErrorCoalesce, LCurly, RCurly, LSquare, RSquare, Equals, Dot, Colon, Comma, StringLiteral, NumberLiteral, PathToken, TrueLiteral, FalseLiteral, NullLiteral, BridgeLexer, } from "./lexer.js";
|
|
9
|
+
import { SELF_MODULE } from "../types.js";
|
|
10
|
+
// ── Reserved-word guards (mirroring the regex parser) ──────────────────────
|
|
11
|
+
const RESERVED_KEYWORDS = new Set([
|
|
12
|
+
"bridge", "with", "as", "from", "const", "tool", "version", "define",
|
|
13
|
+
]);
|
|
14
|
+
const SOURCE_IDENTIFIERS = new Set(["input", "output", "context"]);
|
|
15
|
+
function assertNotReserved(name, lineNum, label) {
|
|
16
|
+
if (RESERVED_KEYWORDS.has(name.toLowerCase())) {
|
|
17
|
+
throw new Error(`Line ${lineNum}: "${name}" is a reserved keyword and cannot be used as a ${label}`);
|
|
18
|
+
}
|
|
19
|
+
if (SOURCE_IDENTIFIERS.has(name.toLowerCase())) {
|
|
20
|
+
throw new Error(`Line ${lineNum}: "${name}" is a reserved source identifier and cannot be used as a ${label}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
// Grammar (CstParser)
|
|
25
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
26
|
+
class BridgeParser extends CstParser {
|
|
27
|
+
constructor() {
|
|
28
|
+
super(allTokens, {
|
|
29
|
+
recoveryEnabled: false,
|
|
30
|
+
maxLookahead: 4,
|
|
31
|
+
});
|
|
32
|
+
this.performSelfAnalysis();
|
|
33
|
+
}
|
|
34
|
+
// ── Top-level ──────────────────────────────────────────────────────────
|
|
35
|
+
program = this.RULE("program", () => {
|
|
36
|
+
this.SUBRULE(this.versionDecl);
|
|
37
|
+
this.MANY(() => {
|
|
38
|
+
this.OR([
|
|
39
|
+
{ ALT: () => this.SUBRULE(this.toolBlock) },
|
|
40
|
+
{ ALT: () => this.SUBRULE(this.bridgeBlock) },
|
|
41
|
+
{ ALT: () => this.SUBRULE(this.defineBlock) },
|
|
42
|
+
{ ALT: () => this.SUBRULE(this.constDecl) },
|
|
43
|
+
]);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
/** version 1.4 */
|
|
47
|
+
versionDecl = this.RULE("versionDecl", () => {
|
|
48
|
+
this.CONSUME(VersionKw);
|
|
49
|
+
this.CONSUME(NumberLiteral, { LABEL: "ver" });
|
|
50
|
+
});
|
|
51
|
+
// ── Tool block ─────────────────────────────────────────────────────────
|
|
52
|
+
toolBlock = this.RULE("toolBlock", () => {
|
|
53
|
+
this.CONSUME(ToolKw);
|
|
54
|
+
this.SUBRULE(this.dottedName, { LABEL: "toolName" });
|
|
55
|
+
this.CONSUME(FromKw);
|
|
56
|
+
this.SUBRULE2(this.dottedName, { LABEL: "toolSource" });
|
|
57
|
+
this.OPTION(() => {
|
|
58
|
+
this.CONSUME(LCurly);
|
|
59
|
+
this.MANY(() => this.SUBRULE(this.toolBodyLine));
|
|
60
|
+
this.CONSUME(RCurly);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
/**
|
|
64
|
+
* A single line inside a tool block.
|
|
65
|
+
*
|
|
66
|
+
* Ambiguity fix: `.target = value` and `.target <- source` share the
|
|
67
|
+
* prefix `Dot dottedPath`, so we merge them into one alternative that
|
|
68
|
+
* parses the prefix then branches on `=` vs `<-`.
|
|
69
|
+
*
|
|
70
|
+
* `on error` and `with` have distinct first tokens so they stay separate.
|
|
71
|
+
*/
|
|
72
|
+
toolBodyLine = this.RULE("toolBodyLine", () => {
|
|
73
|
+
this.OR([
|
|
74
|
+
{ ALT: () => this.SUBRULE(this.toolOnError) },
|
|
75
|
+
{ ALT: () => this.SUBRULE(this.toolWithDecl) },
|
|
76
|
+
{ ALT: () => this.SUBRULE(this.toolWire) }, // merged constant + pull
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
/**
|
|
80
|
+
* Tool wire (merged): .target = value | .target <- source
|
|
81
|
+
*
|
|
82
|
+
* Parses the common prefix `.dottedPath` then branches on operator.
|
|
83
|
+
*/
|
|
84
|
+
toolWire = this.RULE("toolWire", () => {
|
|
85
|
+
this.CONSUME(Dot);
|
|
86
|
+
this.SUBRULE(this.dottedPath, { LABEL: "target" });
|
|
87
|
+
this.OR([
|
|
88
|
+
{
|
|
89
|
+
ALT: () => {
|
|
90
|
+
this.CONSUME(Equals, { LABEL: "equalsOp" });
|
|
91
|
+
this.SUBRULE(this.bareValue, { LABEL: "value" });
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
ALT: () => {
|
|
96
|
+
this.CONSUME(Arrow, { LABEL: "arrowOp" });
|
|
97
|
+
this.SUBRULE(this.dottedName, { LABEL: "source" });
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
]);
|
|
101
|
+
});
|
|
102
|
+
/** on error = <value> | on error <- <source> */
|
|
103
|
+
toolOnError = this.RULE("toolOnError", () => {
|
|
104
|
+
this.CONSUME(OnKw);
|
|
105
|
+
this.CONSUME(ErrorKw);
|
|
106
|
+
this.OR([
|
|
107
|
+
{
|
|
108
|
+
ALT: () => {
|
|
109
|
+
this.CONSUME(Equals, { LABEL: "equalsOp" });
|
|
110
|
+
this.SUBRULE(this.jsonValue, { LABEL: "errorValue" });
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
ALT: () => {
|
|
115
|
+
this.CONSUME(Arrow, { LABEL: "arrowOp" });
|
|
116
|
+
this.SUBRULE(this.dottedName, { LABEL: "errorSource" });
|
|
117
|
+
},
|
|
118
|
+
},
|
|
119
|
+
]);
|
|
120
|
+
});
|
|
121
|
+
/** with context [as alias] | with const [as alias] | with <tool> as <alias> */
|
|
122
|
+
toolWithDecl = this.RULE("toolWithDecl", () => {
|
|
123
|
+
this.CONSUME(WithKw);
|
|
124
|
+
this.OR([
|
|
125
|
+
{
|
|
126
|
+
ALT: () => {
|
|
127
|
+
this.CONSUME(ContextKw, { LABEL: "contextKw" });
|
|
128
|
+
this.OPTION(() => {
|
|
129
|
+
this.CONSUME(AsKw);
|
|
130
|
+
this.SUBRULE(this.nameToken, { LABEL: "alias" });
|
|
131
|
+
});
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
ALT: () => {
|
|
136
|
+
this.CONSUME(ConstKw, { LABEL: "constKw" });
|
|
137
|
+
this.OPTION2(() => {
|
|
138
|
+
this.CONSUME2(AsKw);
|
|
139
|
+
this.SUBRULE2(this.nameToken, { LABEL: "constAlias" });
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
// General tool reference — GATE excludes keywords handled above
|
|
145
|
+
GATE: () => {
|
|
146
|
+
const la = this.LA(1);
|
|
147
|
+
return la.tokenType !== ContextKw && la.tokenType !== ConstKw;
|
|
148
|
+
},
|
|
149
|
+
ALT: () => {
|
|
150
|
+
this.SUBRULE(this.dottedName, { LABEL: "toolName" });
|
|
151
|
+
this.CONSUME3(AsKw);
|
|
152
|
+
this.SUBRULE3(this.nameToken, { LABEL: "toolAlias" });
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
});
|
|
157
|
+
// ── Bridge block ───────────────────────────────────────────────────────
|
|
158
|
+
bridgeBlock = this.RULE("bridgeBlock", () => {
|
|
159
|
+
this.CONSUME(BridgeKw);
|
|
160
|
+
this.SUBRULE(this.nameToken, { LABEL: "typeName" });
|
|
161
|
+
this.CONSUME(Dot);
|
|
162
|
+
this.SUBRULE2(this.nameToken, { LABEL: "fieldName" });
|
|
163
|
+
this.OR([
|
|
164
|
+
{
|
|
165
|
+
// Passthrough shorthand: bridge Type.field with <name>
|
|
166
|
+
ALT: () => {
|
|
167
|
+
this.CONSUME(WithKw, { LABEL: "passthroughWith" });
|
|
168
|
+
this.SUBRULE(this.dottedName, { LABEL: "passthroughName" });
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
// Full bridge block: bridge Type.field { ... }
|
|
173
|
+
ALT: () => {
|
|
174
|
+
this.CONSUME(LCurly);
|
|
175
|
+
this.MANY(() => this.SUBRULE(this.bridgeBodyLine));
|
|
176
|
+
this.CONSUME(RCurly);
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
});
|
|
181
|
+
/**
|
|
182
|
+
* A line inside a bridge/define body.
|
|
183
|
+
*
|
|
184
|
+
* Ambiguity fix: `target = value` and `target <- source` share the prefix
|
|
185
|
+
* `addressPath`, so they're merged into `bridgeWire`.
|
|
186
|
+
* `with` declarations start with WithKw and are unambiguous.
|
|
187
|
+
*/
|
|
188
|
+
bridgeBodyLine = this.RULE("bridgeBodyLine", () => {
|
|
189
|
+
this.OR([
|
|
190
|
+
{ ALT: () => this.SUBRULE(this.bridgeWithDecl) },
|
|
191
|
+
{ ALT: () => this.SUBRULE(this.bridgeWire) }, // merged constant + pull
|
|
192
|
+
]);
|
|
193
|
+
});
|
|
194
|
+
/** with input/output/context/const/tool [as handle] */
|
|
195
|
+
bridgeWithDecl = this.RULE("bridgeWithDecl", () => {
|
|
196
|
+
this.CONSUME(WithKw);
|
|
197
|
+
this.OR([
|
|
198
|
+
{
|
|
199
|
+
ALT: () => {
|
|
200
|
+
this.CONSUME(InputKw, { LABEL: "inputKw" });
|
|
201
|
+
this.OPTION(() => {
|
|
202
|
+
this.CONSUME(AsKw);
|
|
203
|
+
this.SUBRULE(this.nameToken, { LABEL: "inputAlias" });
|
|
204
|
+
});
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
ALT: () => {
|
|
209
|
+
this.CONSUME(OutputKw, { LABEL: "outputKw" });
|
|
210
|
+
this.OPTION2(() => {
|
|
211
|
+
this.CONSUME2(AsKw);
|
|
212
|
+
this.SUBRULE2(this.nameToken, { LABEL: "outputAlias" });
|
|
213
|
+
});
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
ALT: () => {
|
|
218
|
+
this.CONSUME(ContextKw, { LABEL: "contextKw" });
|
|
219
|
+
this.OPTION3(() => {
|
|
220
|
+
this.CONSUME3(AsKw);
|
|
221
|
+
this.SUBRULE3(this.nameToken, { LABEL: "contextAlias" });
|
|
222
|
+
});
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
ALT: () => {
|
|
227
|
+
this.CONSUME(ConstKw, { LABEL: "constKw" });
|
|
228
|
+
this.OPTION4(() => {
|
|
229
|
+
this.CONSUME4(AsKw);
|
|
230
|
+
this.SUBRULE4(this.nameToken, { LABEL: "constAlias" });
|
|
231
|
+
});
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
// tool or define: with <name> [as <handle>]
|
|
236
|
+
// GATE excludes keywords handled by specific alternatives above
|
|
237
|
+
GATE: () => {
|
|
238
|
+
const la = this.LA(1);
|
|
239
|
+
return la.tokenType !== InputKw && la.tokenType !== OutputKw
|
|
240
|
+
&& la.tokenType !== ContextKw && la.tokenType !== ConstKw;
|
|
241
|
+
},
|
|
242
|
+
ALT: () => {
|
|
243
|
+
this.SUBRULE(this.dottedName, { LABEL: "refName" });
|
|
244
|
+
this.OPTION5(() => {
|
|
245
|
+
this.CONSUME5(AsKw);
|
|
246
|
+
this.SUBRULE5(this.nameToken, { LABEL: "refAlias" });
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
]);
|
|
251
|
+
});
|
|
252
|
+
/**
|
|
253
|
+
* Merged bridge wire (constant or pull):
|
|
254
|
+
* target = value
|
|
255
|
+
* target <-[!] sourceExpr [[] as iter { ...elements... }]
|
|
256
|
+
* [|| alt]* [?? fallback]
|
|
257
|
+
*/
|
|
258
|
+
bridgeWire = this.RULE("bridgeWire", () => {
|
|
259
|
+
this.SUBRULE(this.addressPath, { LABEL: "target" });
|
|
260
|
+
this.OR([
|
|
261
|
+
{
|
|
262
|
+
// Constant wire: target = value
|
|
263
|
+
ALT: () => {
|
|
264
|
+
this.CONSUME(Equals, { LABEL: "equalsOp" });
|
|
265
|
+
this.SUBRULE(this.bareValue, { LABEL: "constValue" });
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
// Pull wire: target <-[!] sourceExpr [modifiers]
|
|
270
|
+
ALT: () => {
|
|
271
|
+
this.OR2([
|
|
272
|
+
{ ALT: () => this.CONSUME(Arrow, { LABEL: "arrow" }) },
|
|
273
|
+
{ ALT: () => this.CONSUME(ForceArrow, { LABEL: "forceArrow" }) },
|
|
274
|
+
]);
|
|
275
|
+
this.SUBRULE(this.sourceExpr, { LABEL: "firstSource" });
|
|
276
|
+
// Optional array mapping: [] as <iter> { ... }
|
|
277
|
+
this.OPTION(() => this.SUBRULE(this.arrayMapping));
|
|
278
|
+
// || coalesce chain
|
|
279
|
+
this.MANY(() => {
|
|
280
|
+
this.CONSUME(NullCoalesce);
|
|
281
|
+
this.SUBRULE(this.coalesceAlternative, { LABEL: "nullAlt" });
|
|
282
|
+
});
|
|
283
|
+
// ?? error fallback
|
|
284
|
+
this.OPTION2(() => {
|
|
285
|
+
this.CONSUME(ErrorCoalesce);
|
|
286
|
+
this.SUBRULE2(this.coalesceAlternative, { LABEL: "errorAlt" });
|
|
287
|
+
});
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
});
|
|
292
|
+
/** [] as <iter> { ...element lines... } */
|
|
293
|
+
arrayMapping = this.RULE("arrayMapping", () => {
|
|
294
|
+
this.CONSUME(LSquare);
|
|
295
|
+
this.CONSUME(RSquare);
|
|
296
|
+
this.CONSUME(AsKw);
|
|
297
|
+
this.SUBRULE(this.nameToken, { LABEL: "iterName" });
|
|
298
|
+
this.CONSUME(LCurly);
|
|
299
|
+
this.MANY(() => this.SUBRULE(this.elementLine));
|
|
300
|
+
this.CONSUME(RCurly);
|
|
301
|
+
});
|
|
302
|
+
/**
|
|
303
|
+
* Element line inside array mapping: .field = value | .field <- source [|| ...] [?? ...]
|
|
304
|
+
*/
|
|
305
|
+
elementLine = this.RULE("elementLine", () => {
|
|
306
|
+
this.CONSUME(Dot);
|
|
307
|
+
this.SUBRULE(this.dottedPath, { LABEL: "elemTarget" });
|
|
308
|
+
this.OR([
|
|
309
|
+
{
|
|
310
|
+
ALT: () => {
|
|
311
|
+
this.CONSUME(Equals, { LABEL: "elemEquals" });
|
|
312
|
+
this.SUBRULE(this.bareValue, { LABEL: "elemValue" });
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
ALT: () => {
|
|
317
|
+
this.CONSUME(Arrow, { LABEL: "elemArrow" });
|
|
318
|
+
this.SUBRULE(this.sourceExpr, { LABEL: "elemSource" });
|
|
319
|
+
this.MANY(() => {
|
|
320
|
+
this.CONSUME(NullCoalesce);
|
|
321
|
+
this.SUBRULE(this.coalesceAlternative, { LABEL: "elemNullAlt" });
|
|
322
|
+
});
|
|
323
|
+
this.OPTION(() => {
|
|
324
|
+
this.CONSUME(ErrorCoalesce);
|
|
325
|
+
this.SUBRULE2(this.coalesceAlternative, { LABEL: "elemErrorAlt" });
|
|
326
|
+
});
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
]);
|
|
330
|
+
});
|
|
331
|
+
/** A coalesce alternative: either a JSON literal or a source expression */
|
|
332
|
+
coalesceAlternative = this.RULE("coalesceAlternative", () => {
|
|
333
|
+
// Need to distinguish literal values from source references.
|
|
334
|
+
// Literals start with StringLiteral, NumberLiteral,
|
|
335
|
+
// TrueLiteral, FalseLiteral, NullLiteral, or LCurly (inline JSON object).
|
|
336
|
+
// Sources start with Identifier or keyword-as-name (nameToken) which are
|
|
337
|
+
// handle references.
|
|
338
|
+
//
|
|
339
|
+
// Potential ambiguity: TrueLiteral/FalseLiteral/NullLiteral could be
|
|
340
|
+
// either a literal or a handle name. But the regex parser treats them as
|
|
341
|
+
// literals in || and ?? position (isJsonLiteral check).
|
|
342
|
+
// Identifiers are always source refs. So we use BACKTRACK for safety.
|
|
343
|
+
this.OR([
|
|
344
|
+
{ ALT: () => this.CONSUME(StringLiteral, { LABEL: "stringLit" }) },
|
|
345
|
+
{ ALT: () => this.CONSUME(NumberLiteral, { LABEL: "numberLit" }) },
|
|
346
|
+
{ ALT: () => this.CONSUME(TrueLiteral, { LABEL: "trueLit" }) },
|
|
347
|
+
{ ALT: () => this.CONSUME(FalseLiteral, { LABEL: "falseLit" }) },
|
|
348
|
+
{ ALT: () => this.CONSUME(NullLiteral, { LABEL: "nullLit" }) },
|
|
349
|
+
{ ALT: () => this.SUBRULE(this.jsonInlineObject, { LABEL: "objectLit" }) },
|
|
350
|
+
{ ALT: () => this.SUBRULE(this.sourceExpr, { LABEL: "sourceAlt" }) },
|
|
351
|
+
]);
|
|
352
|
+
});
|
|
353
|
+
// ── Define block ───────────────────────────────────────────────────────
|
|
354
|
+
defineBlock = this.RULE("defineBlock", () => {
|
|
355
|
+
this.CONSUME(DefineKw);
|
|
356
|
+
this.SUBRULE(this.nameToken, { LABEL: "defineName" });
|
|
357
|
+
this.CONSUME(LCurly);
|
|
358
|
+
this.MANY(() => this.SUBRULE(this.bridgeBodyLine));
|
|
359
|
+
this.CONSUME(RCurly);
|
|
360
|
+
});
|
|
361
|
+
// ── Const declaration ──────────────────────────────────────────────────
|
|
362
|
+
/** const <name> = <jsonValue> */
|
|
363
|
+
constDecl = this.RULE("constDecl", () => {
|
|
364
|
+
this.CONSUME(ConstKw);
|
|
365
|
+
this.SUBRULE(this.nameToken, { LABEL: "constName" });
|
|
366
|
+
this.CONSUME(Equals);
|
|
367
|
+
this.SUBRULE(this.jsonValue, { LABEL: "constValue" });
|
|
368
|
+
});
|
|
369
|
+
// ── Shared sub-rules ──────────────────────────────────────────────────
|
|
370
|
+
/** Source expression: [pipe:]*address (pipe chain or simple ref) */
|
|
371
|
+
sourceExpr = this.RULE("sourceExpr", () => {
|
|
372
|
+
this.SUBRULE(this.addressPath, { LABEL: "head" });
|
|
373
|
+
this.MANY(() => {
|
|
374
|
+
this.CONSUME(Colon);
|
|
375
|
+
this.SUBRULE2(this.addressPath, { LABEL: "pipeSegment" });
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
/**
|
|
379
|
+
* Address path: a dotted reference with optional array indices.
|
|
380
|
+
* Examples: o.lat, i.name, g.items[0].position.lat, o
|
|
381
|
+
*
|
|
382
|
+
* Note: empty brackets `[]` are NOT consumed here — they belong to
|
|
383
|
+
* the array mapping rule. The GATE on MANY prevents entering when `[`
|
|
384
|
+
* is followed by `]` (empty brackets).
|
|
385
|
+
*
|
|
386
|
+
* Line-boundary guard: stops consuming dots that cross a newline,
|
|
387
|
+
* so `.id` on the next line isn't greedily absorbed as a path continuation
|
|
388
|
+
* inside element blocks.
|
|
389
|
+
*/
|
|
390
|
+
addressPath = this.RULE("addressPath", () => {
|
|
391
|
+
this.SUBRULE(this.nameToken, { LABEL: "root" });
|
|
392
|
+
this.MANY({
|
|
393
|
+
GATE: () => {
|
|
394
|
+
const la = this.LA(1);
|
|
395
|
+
if (la.tokenType === Dot) {
|
|
396
|
+
// Don't continue across a line break — prevents greedy path
|
|
397
|
+
// consumption in multi-line contexts like element blocks.
|
|
398
|
+
// LA(0) gives the last consumed token.
|
|
399
|
+
const prev = this.LA(0);
|
|
400
|
+
if (prev && la.startLine != null && prev.endLine != null
|
|
401
|
+
&& la.startLine > prev.endLine) {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
if (la.tokenType === LSquare) {
|
|
407
|
+
const la2 = this.LA(2);
|
|
408
|
+
return la2.tokenType === NumberLiteral;
|
|
409
|
+
}
|
|
410
|
+
return false;
|
|
411
|
+
},
|
|
412
|
+
DEF: () => {
|
|
413
|
+
this.OR([
|
|
414
|
+
{
|
|
415
|
+
ALT: () => {
|
|
416
|
+
this.CONSUME(Dot);
|
|
417
|
+
this.SUBRULE(this.pathSegment, { LABEL: "segment" });
|
|
418
|
+
},
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
ALT: () => {
|
|
422
|
+
this.CONSUME(LSquare);
|
|
423
|
+
this.CONSUME(NumberLiteral, { LABEL: "arrayIndex" });
|
|
424
|
+
this.CONSUME(RSquare);
|
|
425
|
+
},
|
|
426
|
+
},
|
|
427
|
+
]);
|
|
428
|
+
},
|
|
429
|
+
});
|
|
430
|
+
});
|
|
431
|
+
/** Segment after a dot: any identifier or keyword usable in a path */
|
|
432
|
+
pathSegment = this.RULE("pathSegment", () => {
|
|
433
|
+
this.OR([
|
|
434
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
435
|
+
{ ALT: () => this.CONSUME(InputKw) },
|
|
436
|
+
{ ALT: () => this.CONSUME(OutputKw) },
|
|
437
|
+
{ ALT: () => this.CONSUME(ContextKw) },
|
|
438
|
+
{ ALT: () => this.CONSUME(ConstKw) },
|
|
439
|
+
{ ALT: () => this.CONSUME(ErrorKw) },
|
|
440
|
+
{ ALT: () => this.CONSUME(OnKw) },
|
|
441
|
+
{ ALT: () => this.CONSUME(FromKw) },
|
|
442
|
+
{ ALT: () => this.CONSUME(AsKw) },
|
|
443
|
+
{ ALT: () => this.CONSUME(ToolKw) },
|
|
444
|
+
{ ALT: () => this.CONSUME(BridgeKw) },
|
|
445
|
+
{ ALT: () => this.CONSUME(DefineKw) },
|
|
446
|
+
{ ALT: () => this.CONSUME(WithKw) },
|
|
447
|
+
{ ALT: () => this.CONSUME(VersionKw) },
|
|
448
|
+
{ ALT: () => this.CONSUME(TrueLiteral) },
|
|
449
|
+
{ ALT: () => this.CONSUME(FalseLiteral) },
|
|
450
|
+
{ ALT: () => this.CONSUME(NullLiteral) },
|
|
451
|
+
]);
|
|
452
|
+
});
|
|
453
|
+
/** Dotted name: identifier segments separated by dots */
|
|
454
|
+
dottedName = this.RULE("dottedName", () => {
|
|
455
|
+
this.SUBRULE(this.nameToken, { LABEL: "first" });
|
|
456
|
+
this.MANY({
|
|
457
|
+
GATE: () => {
|
|
458
|
+
const la = this.LA(1);
|
|
459
|
+
if (la.tokenType !== Dot)
|
|
460
|
+
return false;
|
|
461
|
+
const prev = this.LA(0);
|
|
462
|
+
if (prev && la.startLine != null && prev.endLine != null
|
|
463
|
+
&& la.startLine > prev.endLine)
|
|
464
|
+
return false;
|
|
465
|
+
return true;
|
|
466
|
+
},
|
|
467
|
+
DEF: () => {
|
|
468
|
+
this.CONSUME(Dot);
|
|
469
|
+
this.SUBRULE2(this.nameToken, { LABEL: "rest" });
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
/** Dotted path (within tool block): segments after a leading dot */
|
|
474
|
+
dottedPath = this.RULE("dottedPath", () => {
|
|
475
|
+
this.SUBRULE(this.pathSegment, { LABEL: "first" });
|
|
476
|
+
this.MANY({
|
|
477
|
+
GATE: () => {
|
|
478
|
+
const la = this.LA(1);
|
|
479
|
+
if (la.tokenType !== Dot)
|
|
480
|
+
return false;
|
|
481
|
+
const prev = this.LA(0);
|
|
482
|
+
if (prev && la.startLine != null && prev.endLine != null
|
|
483
|
+
&& la.startLine > prev.endLine)
|
|
484
|
+
return false;
|
|
485
|
+
return true;
|
|
486
|
+
},
|
|
487
|
+
DEF: () => {
|
|
488
|
+
this.CONSUME(Dot);
|
|
489
|
+
this.SUBRULE2(this.pathSegment, { LABEL: "rest" });
|
|
490
|
+
},
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
/** A name token: Identifier or certain keywords usable as names.
|
|
494
|
+
* Note: true/false/null are NOT allowed here to avoid ambiguity with
|
|
495
|
+
* literals in coalesceAlternative. They ARE allowed in pathSegment. */
|
|
496
|
+
nameToken = this.RULE("nameToken", () => {
|
|
497
|
+
this.OR([
|
|
498
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
499
|
+
{ ALT: () => this.CONSUME(InputKw) },
|
|
500
|
+
{ ALT: () => this.CONSUME(OutputKw) },
|
|
501
|
+
{ ALT: () => this.CONSUME(ContextKw) },
|
|
502
|
+
{ ALT: () => this.CONSUME(ConstKw) },
|
|
503
|
+
{ ALT: () => this.CONSUME(ErrorKw) },
|
|
504
|
+
{ ALT: () => this.CONSUME(OnKw) },
|
|
505
|
+
{ ALT: () => this.CONSUME(FromKw) },
|
|
506
|
+
{ ALT: () => this.CONSUME(AsKw) },
|
|
507
|
+
{ ALT: () => this.CONSUME(ToolKw) },
|
|
508
|
+
{ ALT: () => this.CONSUME(BridgeKw) },
|
|
509
|
+
{ ALT: () => this.CONSUME(DefineKw) },
|
|
510
|
+
{ ALT: () => this.CONSUME(WithKw) },
|
|
511
|
+
{ ALT: () => this.CONSUME(VersionKw) },
|
|
512
|
+
]);
|
|
513
|
+
});
|
|
514
|
+
/** Bare value: string, number, path, boolean, null, or unquoted identifier */
|
|
515
|
+
bareValue = this.RULE("bareValue", () => {
|
|
516
|
+
this.OR([
|
|
517
|
+
{ ALT: () => this.CONSUME(StringLiteral) },
|
|
518
|
+
{ ALT: () => this.CONSUME(NumberLiteral) },
|
|
519
|
+
{ ALT: () => this.CONSUME(PathToken) },
|
|
520
|
+
{ ALT: () => this.CONSUME(TrueLiteral) },
|
|
521
|
+
{ ALT: () => this.CONSUME(FalseLiteral) },
|
|
522
|
+
{ ALT: () => this.CONSUME(NullLiteral) },
|
|
523
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
524
|
+
{ ALT: () => this.CONSUME(InputKw) },
|
|
525
|
+
{ ALT: () => this.CONSUME(OutputKw) },
|
|
526
|
+
{ ALT: () => this.CONSUME(ErrorKw) },
|
|
527
|
+
{ ALT: () => this.CONSUME(OnKw) },
|
|
528
|
+
{ ALT: () => this.CONSUME(FromKw) },
|
|
529
|
+
{ ALT: () => this.CONSUME(AsKw) },
|
|
530
|
+
]);
|
|
531
|
+
});
|
|
532
|
+
/** JSON value: string, number, boolean, null, object, or array */
|
|
533
|
+
jsonValue = this.RULE("jsonValue", () => {
|
|
534
|
+
this.OR([
|
|
535
|
+
{ ALT: () => this.CONSUME(StringLiteral, { LABEL: "string" }) },
|
|
536
|
+
{ ALT: () => this.CONSUME(NumberLiteral, { LABEL: "number" }) },
|
|
537
|
+
{ ALT: () => this.CONSUME(TrueLiteral, { LABEL: "true" }) },
|
|
538
|
+
{ ALT: () => this.CONSUME(FalseLiteral, { LABEL: "false" }) },
|
|
539
|
+
{ ALT: () => this.CONSUME(NullLiteral, { LABEL: "null" }) },
|
|
540
|
+
{ ALT: () => this.SUBRULE(this.jsonObject, { LABEL: "object" }) },
|
|
541
|
+
{ ALT: () => this.SUBRULE(this.jsonArray, { LABEL: "array" }) },
|
|
542
|
+
]);
|
|
543
|
+
});
|
|
544
|
+
/** JSON object: { ... } — we accept any tokens inside and reconstruct in the visitor */
|
|
545
|
+
jsonObject = this.RULE("jsonObject", () => {
|
|
546
|
+
this.CONSUME(LCurly);
|
|
547
|
+
this.MANY(() => {
|
|
548
|
+
this.OR([
|
|
549
|
+
{ ALT: () => this.CONSUME(StringLiteral) },
|
|
550
|
+
{ ALT: () => this.CONSUME(NumberLiteral) },
|
|
551
|
+
{ ALT: () => this.CONSUME(Colon) },
|
|
552
|
+
{ ALT: () => this.CONSUME(Comma) },
|
|
553
|
+
{ ALT: () => this.CONSUME(TrueLiteral) },
|
|
554
|
+
{ ALT: () => this.CONSUME(FalseLiteral) },
|
|
555
|
+
{ ALT: () => this.CONSUME(NullLiteral) },
|
|
556
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
557
|
+
{ ALT: () => this.CONSUME(LSquare) },
|
|
558
|
+
{ ALT: () => this.CONSUME(RSquare) },
|
|
559
|
+
{ ALT: () => this.CONSUME(Dot) },
|
|
560
|
+
{ ALT: () => this.CONSUME(Equals) },
|
|
561
|
+
// Nested objects
|
|
562
|
+
{ ALT: () => this.SUBRULE(this.jsonObject) },
|
|
563
|
+
]);
|
|
564
|
+
});
|
|
565
|
+
this.CONSUME(RCurly);
|
|
566
|
+
});
|
|
567
|
+
/** JSON array: [ ... ] */
|
|
568
|
+
jsonArray = this.RULE("jsonArray", () => {
|
|
569
|
+
this.CONSUME(LSquare);
|
|
570
|
+
this.MANY(() => {
|
|
571
|
+
this.OR([
|
|
572
|
+
{ ALT: () => this.CONSUME(StringLiteral) },
|
|
573
|
+
{ ALT: () => this.CONSUME(NumberLiteral) },
|
|
574
|
+
{ ALT: () => this.CONSUME(Colon) },
|
|
575
|
+
{ ALT: () => this.CONSUME(Comma) },
|
|
576
|
+
{ ALT: () => this.CONSUME(TrueLiteral) },
|
|
577
|
+
{ ALT: () => this.CONSUME(FalseLiteral) },
|
|
578
|
+
{ ALT: () => this.CONSUME(NullLiteral) },
|
|
579
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
580
|
+
{ ALT: () => this.CONSUME(Dot) },
|
|
581
|
+
{ ALT: () => this.SUBRULE(this.jsonObject) },
|
|
582
|
+
{ ALT: () => this.SUBRULE(this.jsonArray) },
|
|
583
|
+
]);
|
|
584
|
+
});
|
|
585
|
+
this.CONSUME(RSquare);
|
|
586
|
+
});
|
|
587
|
+
/** Inline JSON object — used in coalesce alternatives */
|
|
588
|
+
jsonInlineObject = this.RULE("jsonInlineObject", () => {
|
|
589
|
+
this.CONSUME(LCurly);
|
|
590
|
+
this.MANY(() => {
|
|
591
|
+
this.OR([
|
|
592
|
+
{ ALT: () => this.CONSUME(StringLiteral) },
|
|
593
|
+
{ ALT: () => this.CONSUME(NumberLiteral) },
|
|
594
|
+
{ ALT: () => this.CONSUME(Colon) },
|
|
595
|
+
{ ALT: () => this.CONSUME(Comma) },
|
|
596
|
+
{ ALT: () => this.CONSUME(TrueLiteral) },
|
|
597
|
+
{ ALT: () => this.CONSUME(FalseLiteral) },
|
|
598
|
+
{ ALT: () => this.CONSUME(NullLiteral) },
|
|
599
|
+
{ ALT: () => this.CONSUME(Identifier) },
|
|
600
|
+
{ ALT: () => this.CONSUME(LSquare) },
|
|
601
|
+
{ ALT: () => this.CONSUME(RSquare) },
|
|
602
|
+
{ ALT: () => this.CONSUME(Dot) },
|
|
603
|
+
{ ALT: () => this.CONSUME(Equals) },
|
|
604
|
+
{ ALT: () => this.SUBRULE(this.jsonInlineObject) },
|
|
605
|
+
]);
|
|
606
|
+
});
|
|
607
|
+
this.CONSUME(RCurly);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
// Singleton parser instance (Chevrotain best practice)
|
|
611
|
+
const parserInstance = new BridgeParser();
|
|
612
|
+
const BRIDGE_VERSION = "1.4";
|
|
613
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
614
|
+
// Public API
|
|
615
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
616
|
+
export function parseBridgeChevrotain(text) {
|
|
617
|
+
return internalParse(text);
|
|
618
|
+
}
|
|
619
|
+
function internalParse(text, previousInstructions) {
|
|
620
|
+
// 1. Lex
|
|
621
|
+
const lexResult = BridgeLexer.tokenize(text);
|
|
622
|
+
if (lexResult.errors.length > 0) {
|
|
623
|
+
const e = lexResult.errors[0];
|
|
624
|
+
throw new Error(`Line ${e.line}: Unexpected character "${e.message}"`);
|
|
625
|
+
}
|
|
626
|
+
// 2. Parse
|
|
627
|
+
parserInstance.input = lexResult.tokens;
|
|
628
|
+
const cst = parserInstance.program();
|
|
629
|
+
if (parserInstance.errors.length > 0) {
|
|
630
|
+
const e = parserInstance.errors[0];
|
|
631
|
+
throw new Error(e.message);
|
|
632
|
+
}
|
|
633
|
+
// 3. Visit → AST
|
|
634
|
+
return toBridgeAst(cst, previousInstructions);
|
|
635
|
+
}
|
|
636
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
637
|
+
// CST → AST transformation (imperative visitor)
|
|
638
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
639
|
+
// ── Token / CST node helpers ────────────────────────────────────────────
|
|
640
|
+
function sub(node, ruleName) {
|
|
641
|
+
const nodes = node.children[ruleName];
|
|
642
|
+
return nodes?.[0];
|
|
643
|
+
}
|
|
644
|
+
function subs(node, ruleName) {
|
|
645
|
+
return node.children[ruleName] ?? [];
|
|
646
|
+
}
|
|
647
|
+
function tok(node, tokenName) {
|
|
648
|
+
const tokens = node.children[tokenName];
|
|
649
|
+
return tokens?.[0];
|
|
650
|
+
}
|
|
651
|
+
function toks(node, tokenName) {
|
|
652
|
+
return node.children[tokenName] ?? [];
|
|
653
|
+
}
|
|
654
|
+
function line(token) {
|
|
655
|
+
return token?.startLine ?? 0;
|
|
656
|
+
}
|
|
657
|
+
/* ── extractNameToken: get string from nameToken CST node ── */
|
|
658
|
+
function extractNameToken(node) {
|
|
659
|
+
const c = node.children;
|
|
660
|
+
for (const key of Object.keys(c)) {
|
|
661
|
+
const tokens = c[key];
|
|
662
|
+
if (tokens?.[0])
|
|
663
|
+
return tokens[0].image;
|
|
664
|
+
}
|
|
665
|
+
return "";
|
|
666
|
+
}
|
|
667
|
+
/* ── extractDottedName: reassemble from dottedName CST node ── */
|
|
668
|
+
function extractDottedName(node) {
|
|
669
|
+
const first = extractNameToken(sub(node, "first"));
|
|
670
|
+
const rest = subs(node, "rest").map(n => extractNameToken(n));
|
|
671
|
+
return [first, ...rest].join(".");
|
|
672
|
+
}
|
|
673
|
+
/* ── extractPathSegment: get string from pathSegment ── */
|
|
674
|
+
function extractPathSegment(node) {
|
|
675
|
+
for (const key of Object.keys(node.children)) {
|
|
676
|
+
const tokens = node.children[key];
|
|
677
|
+
if (tokens?.[0])
|
|
678
|
+
return tokens[0].image;
|
|
679
|
+
}
|
|
680
|
+
return "";
|
|
681
|
+
}
|
|
682
|
+
/* ── extractDottedPathStr: reassemble from dottedPath CST node ── */
|
|
683
|
+
function extractDottedPathStr(node) {
|
|
684
|
+
const first = extractPathSegment(sub(node, "first"));
|
|
685
|
+
const rest = subs(node, "rest").map(n => extractPathSegment(n));
|
|
686
|
+
return [first, ...rest].join(".");
|
|
687
|
+
}
|
|
688
|
+
/* ── extractAddressPath: get root + segments preserving order ── */
|
|
689
|
+
function extractAddressPath(node) {
|
|
690
|
+
const root = extractNameToken(sub(node, "root"));
|
|
691
|
+
const items = [];
|
|
692
|
+
for (const seg of subs(node, "segment")) {
|
|
693
|
+
const firstTok = findFirstToken(seg);
|
|
694
|
+
items.push({ offset: firstTok?.startOffset ?? 0, value: extractPathSegment(seg) });
|
|
695
|
+
}
|
|
696
|
+
for (const idxTok of toks(node, "arrayIndex")) {
|
|
697
|
+
if (idxTok.image.includes(".")) {
|
|
698
|
+
throw new Error(`Line ${idxTok.startLine}: Array indices must be integers, found "${idxTok.image}"`);
|
|
699
|
+
}
|
|
700
|
+
items.push({ offset: idxTok.startOffset, value: idxTok.image });
|
|
701
|
+
}
|
|
702
|
+
items.sort((a, b) => a.offset - b.offset);
|
|
703
|
+
return { root, segments: items.map(i => i.value) };
|
|
704
|
+
}
|
|
705
|
+
function findFirstToken(node) {
|
|
706
|
+
for (const key of Object.keys(node.children)) {
|
|
707
|
+
const child = node.children[key];
|
|
708
|
+
if (Array.isArray(child) && child.length > 0) {
|
|
709
|
+
const first = child[0];
|
|
710
|
+
if ("image" in first)
|
|
711
|
+
return first;
|
|
712
|
+
if ("children" in first)
|
|
713
|
+
return findFirstToken(first);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
function stripQuotes(s) {
|
|
719
|
+
if (s.startsWith('"') && s.endsWith('"'))
|
|
720
|
+
return s.slice(1, -1);
|
|
721
|
+
return s;
|
|
722
|
+
}
|
|
723
|
+
/* ── parsePath: split "a.b[0].c" → ["a","b","0","c"] ── */
|
|
724
|
+
function parsePath(text) {
|
|
725
|
+
const parts = [];
|
|
726
|
+
for (const segment of text.split(".")) {
|
|
727
|
+
const match = segment.match(/^([^[]+)(?:\[(\d*)\])?$/);
|
|
728
|
+
if (match) {
|
|
729
|
+
parts.push(match[1]);
|
|
730
|
+
if (match[2] !== undefined && match[2] !== "")
|
|
731
|
+
parts.push(match[2]);
|
|
732
|
+
}
|
|
733
|
+
else {
|
|
734
|
+
parts.push(segment);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
return parts;
|
|
738
|
+
}
|
|
739
|
+
/* ── Collect all tokens recursively from a CST node ── */
|
|
740
|
+
function collectTokens(node, out) {
|
|
741
|
+
for (const key of Object.keys(node.children)) {
|
|
742
|
+
const children = node.children[key];
|
|
743
|
+
if (!Array.isArray(children))
|
|
744
|
+
continue;
|
|
745
|
+
for (const child of children) {
|
|
746
|
+
if ("image" in child)
|
|
747
|
+
out.push(child);
|
|
748
|
+
else if ("children" in child)
|
|
749
|
+
collectTokens(child, out);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
function reconstructJson(node) {
|
|
754
|
+
const tokens = [];
|
|
755
|
+
collectTokens(node, tokens);
|
|
756
|
+
tokens.sort((a, b) => a.startOffset - b.startOffset);
|
|
757
|
+
// Reconstruct with original spacing preserved (using offsets to insert whitespace)
|
|
758
|
+
if (tokens.length === 0)
|
|
759
|
+
return "";
|
|
760
|
+
let result = tokens[0].image;
|
|
761
|
+
for (let i = 1; i < tokens.length; i++) {
|
|
762
|
+
const gap = tokens[i].startOffset - (tokens[i - 1].startOffset + tokens[i - 1].image.length);
|
|
763
|
+
if (gap > 0)
|
|
764
|
+
result += " ".repeat(gap);
|
|
765
|
+
result += tokens[i].image;
|
|
766
|
+
}
|
|
767
|
+
return result;
|
|
768
|
+
}
|
|
769
|
+
/* ── extractBareValue: get the string from a bareValue CST node ── */
|
|
770
|
+
function extractBareValue(node) {
|
|
771
|
+
for (const key of Object.keys(node.children)) {
|
|
772
|
+
const tokens = node.children[key];
|
|
773
|
+
if (tokens?.[0]) {
|
|
774
|
+
let val = tokens[0].image;
|
|
775
|
+
if (val.startsWith('"') && val.endsWith('"'))
|
|
776
|
+
val = val.slice(1, -1);
|
|
777
|
+
return val;
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
return "";
|
|
781
|
+
}
|
|
782
|
+
/* ── extractJsonValue: from a jsonValue CST node ── */
|
|
783
|
+
function extractJsonValue(node) {
|
|
784
|
+
const c = node.children;
|
|
785
|
+
if (c.string)
|
|
786
|
+
return c.string[0].image; // keep quotes for JSON.parse
|
|
787
|
+
if (c.number)
|
|
788
|
+
return c.number[0].image;
|
|
789
|
+
if (c.integer)
|
|
790
|
+
return c.integer[0].image;
|
|
791
|
+
if (c.true)
|
|
792
|
+
return "true";
|
|
793
|
+
if (c.false)
|
|
794
|
+
return "false";
|
|
795
|
+
if (c.null)
|
|
796
|
+
return "null";
|
|
797
|
+
if (c.object)
|
|
798
|
+
return reconstructJson(c.object[0]);
|
|
799
|
+
if (c.array)
|
|
800
|
+
return reconstructJson(c.array[0]);
|
|
801
|
+
return "";
|
|
802
|
+
}
|
|
803
|
+
/* ── extractJsonValueStr: same as above but strips outer quotes for const values ── */
|
|
804
|
+
function extractJsonValueStripped(node) {
|
|
805
|
+
const c = node.children;
|
|
806
|
+
if (c.string)
|
|
807
|
+
return stripQuotes(c.string[0].image);
|
|
808
|
+
if (c.number)
|
|
809
|
+
return c.number[0].image;
|
|
810
|
+
if (c.integer)
|
|
811
|
+
return c.integer[0].image;
|
|
812
|
+
if (c.true)
|
|
813
|
+
return "true";
|
|
814
|
+
if (c.false)
|
|
815
|
+
return "false";
|
|
816
|
+
if (c.null)
|
|
817
|
+
return "null";
|
|
818
|
+
if (c.object)
|
|
819
|
+
return reconstructJson(c.object[0]);
|
|
820
|
+
if (c.array)
|
|
821
|
+
return reconstructJson(c.array[0]);
|
|
822
|
+
return "";
|
|
823
|
+
}
|
|
824
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
825
|
+
// Main AST builder
|
|
826
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
827
|
+
function toBridgeAst(cst, previousInstructions) {
|
|
828
|
+
const instructions = [];
|
|
829
|
+
// If called from passthrough expansion, seed with prior context
|
|
830
|
+
const contextInstructions = previousInstructions
|
|
831
|
+
? [...previousInstructions]
|
|
832
|
+
: [];
|
|
833
|
+
// ── Version check ──
|
|
834
|
+
const versionDecl = sub(cst, "versionDecl");
|
|
835
|
+
if (!versionDecl) {
|
|
836
|
+
throw new Error(`Missing version declaration. Bridge files must begin with: version ${BRIDGE_VERSION}`);
|
|
837
|
+
}
|
|
838
|
+
const versionTok = tok(versionDecl, "ver");
|
|
839
|
+
const versionNum = versionTok?.image;
|
|
840
|
+
if (versionNum !== BRIDGE_VERSION) {
|
|
841
|
+
throw new Error(`Unsupported bridge version "${versionNum}". This parser requires: version ${BRIDGE_VERSION}`);
|
|
842
|
+
}
|
|
843
|
+
const tagged = [];
|
|
844
|
+
for (const n of subs(cst, "constDecl"))
|
|
845
|
+
tagged.push({ offset: findFirstToken(n)?.startOffset ?? 0, kind: "const", node: n });
|
|
846
|
+
for (const n of subs(cst, "toolBlock"))
|
|
847
|
+
tagged.push({ offset: findFirstToken(n)?.startOffset ?? 0, kind: "tool", node: n });
|
|
848
|
+
for (const n of subs(cst, "defineBlock"))
|
|
849
|
+
tagged.push({ offset: findFirstToken(n)?.startOffset ?? 0, kind: "define", node: n });
|
|
850
|
+
for (const n of subs(cst, "bridgeBlock"))
|
|
851
|
+
tagged.push({ offset: findFirstToken(n)?.startOffset ?? 0, kind: "bridge", node: n });
|
|
852
|
+
tagged.sort((a, b) => a.offset - b.offset);
|
|
853
|
+
for (const item of tagged) {
|
|
854
|
+
switch (item.kind) {
|
|
855
|
+
case "const":
|
|
856
|
+
instructions.push(buildConstDef(item.node));
|
|
857
|
+
break;
|
|
858
|
+
case "tool":
|
|
859
|
+
instructions.push(buildToolDef(item.node, [...contextInstructions, ...instructions]));
|
|
860
|
+
break;
|
|
861
|
+
case "define":
|
|
862
|
+
instructions.push(buildDefineDef(item.node));
|
|
863
|
+
break;
|
|
864
|
+
case "bridge":
|
|
865
|
+
instructions.push(...buildBridge(item.node, [...contextInstructions, ...instructions]));
|
|
866
|
+
break;
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return instructions;
|
|
870
|
+
}
|
|
871
|
+
// ── Const ───────────────────────────────────────────────────────────────
|
|
872
|
+
function buildConstDef(node) {
|
|
873
|
+
const nameNode = sub(node, "constName");
|
|
874
|
+
const name = extractNameToken(nameNode);
|
|
875
|
+
const lineNum = line(findFirstToken(nameNode));
|
|
876
|
+
assertNotReserved(name, lineNum, "const name");
|
|
877
|
+
const valueNode = sub(node, "constValue");
|
|
878
|
+
const raw = extractJsonValue(valueNode);
|
|
879
|
+
// Validate JSON
|
|
880
|
+
try {
|
|
881
|
+
JSON.parse(raw);
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
throw new Error(`Line ${lineNum}: Invalid JSON value for const "${name}": ${raw}`);
|
|
885
|
+
}
|
|
886
|
+
return { kind: "const", name, value: raw };
|
|
887
|
+
}
|
|
888
|
+
// ── Tool ────────────────────────────────────────────────────────────────
|
|
889
|
+
function buildToolDef(node, previousInstructions) {
|
|
890
|
+
const toolName = extractDottedName(sub(node, "toolName"));
|
|
891
|
+
const source = extractDottedName(sub(node, "toolSource"));
|
|
892
|
+
const lineNum = line(findFirstToken(sub(node, "toolName")));
|
|
893
|
+
assertNotReserved(toolName, lineNum, "tool name");
|
|
894
|
+
const isKnownTool = previousInstructions.some(inst => inst.kind === "tool" && inst.name === source);
|
|
895
|
+
const deps = [];
|
|
896
|
+
const wires = [];
|
|
897
|
+
for (const bodyLine of subs(node, "toolBodyLine")) {
|
|
898
|
+
const c = bodyLine.children;
|
|
899
|
+
// toolWithDecl
|
|
900
|
+
const withNode = c.toolWithDecl?.[0];
|
|
901
|
+
if (withNode) {
|
|
902
|
+
const wc = withNode.children;
|
|
903
|
+
if (wc.contextKw) {
|
|
904
|
+
const alias = wc.alias ? extractNameToken(wc.alias[0]) : "context";
|
|
905
|
+
deps.push({ kind: "context", handle: alias });
|
|
906
|
+
}
|
|
907
|
+
else if (wc.constKw) {
|
|
908
|
+
const alias = wc.constAlias ? extractNameToken(wc.constAlias[0]) : "const";
|
|
909
|
+
deps.push({ kind: "const", handle: alias });
|
|
910
|
+
}
|
|
911
|
+
else if (wc.toolName) {
|
|
912
|
+
const tName = extractDottedName(wc.toolName[0]);
|
|
913
|
+
const tAlias = extractNameToken(wc.toolAlias[0]);
|
|
914
|
+
deps.push({ kind: "tool", handle: tAlias, tool: tName });
|
|
915
|
+
}
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
// toolOnError
|
|
919
|
+
const onError = c.toolOnError?.[0];
|
|
920
|
+
if (onError) {
|
|
921
|
+
const oc = onError.children;
|
|
922
|
+
if (oc.equalsOp) {
|
|
923
|
+
const value = extractJsonValue(sub(onError, "errorValue"));
|
|
924
|
+
wires.push({ kind: "onError", value });
|
|
925
|
+
}
|
|
926
|
+
else if (oc.arrowOp) {
|
|
927
|
+
const source = extractDottedName(sub(onError, "errorSource"));
|
|
928
|
+
wires.push({ kind: "onError", source });
|
|
929
|
+
}
|
|
930
|
+
continue;
|
|
931
|
+
}
|
|
932
|
+
// toolWire (merged constant + pull)
|
|
933
|
+
const wireNode = c.toolWire?.[0];
|
|
934
|
+
if (wireNode) {
|
|
935
|
+
const wc = wireNode.children;
|
|
936
|
+
const target = extractDottedPathStr(sub(wireNode, "target"));
|
|
937
|
+
if (wc.equalsOp) {
|
|
938
|
+
const value = extractBareValue(sub(wireNode, "value"));
|
|
939
|
+
wires.push({ target, kind: "constant", value });
|
|
940
|
+
}
|
|
941
|
+
else if (wc.arrowOp) {
|
|
942
|
+
const source = extractDottedName(sub(wireNode, "source"));
|
|
943
|
+
wires.push({ target, kind: "pull", source });
|
|
944
|
+
}
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
return {
|
|
949
|
+
kind: "tool",
|
|
950
|
+
name: toolName,
|
|
951
|
+
fn: isKnownTool ? undefined : source,
|
|
952
|
+
extends: isKnownTool ? source : undefined,
|
|
953
|
+
deps,
|
|
954
|
+
wires,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
// ── Define ──────────────────────────────────────────────────────────────
|
|
958
|
+
function buildDefineDef(node) {
|
|
959
|
+
const name = extractNameToken(sub(node, "defineName"));
|
|
960
|
+
const lineNum = line(findFirstToken(sub(node, "defineName")));
|
|
961
|
+
assertNotReserved(name, lineNum, "define name");
|
|
962
|
+
const bodyLines = subs(node, "bridgeBodyLine");
|
|
963
|
+
const { handles, wires, arrayIterators, pipeHandles } = buildBridgeBody(bodyLines, "Define", name, [], lineNum);
|
|
964
|
+
return {
|
|
965
|
+
kind: "define",
|
|
966
|
+
name,
|
|
967
|
+
handles,
|
|
968
|
+
wires,
|
|
969
|
+
...(Object.keys(arrayIterators).length > 0 ? { arrayIterators } : {}),
|
|
970
|
+
...(pipeHandles.length > 0 ? { pipeHandles } : {}),
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
// ── Bridge ──────────────────────────────────────────────────────────────
|
|
974
|
+
function buildBridge(node, previousInstructions) {
|
|
975
|
+
const typeName = extractNameToken(sub(node, "typeName"));
|
|
976
|
+
const fieldName = extractNameToken(sub(node, "fieldName"));
|
|
977
|
+
// Passthrough shorthand
|
|
978
|
+
if (node.children.passthroughWith) {
|
|
979
|
+
const passthroughName = extractDottedName(sub(node, "passthroughName"));
|
|
980
|
+
const sHandle = passthroughName.includes(".")
|
|
981
|
+
? passthroughName.substring(passthroughName.lastIndexOf(".") + 1)
|
|
982
|
+
: passthroughName;
|
|
983
|
+
const expandedText = [
|
|
984
|
+
`version ${BRIDGE_VERSION}`,
|
|
985
|
+
`bridge ${typeName}.${fieldName} {`,
|
|
986
|
+
` with ${passthroughName} as ${sHandle}`,
|
|
987
|
+
` with input`,
|
|
988
|
+
` with output as __out`,
|
|
989
|
+
` ${sHandle} <- input`,
|
|
990
|
+
` __out <- ${sHandle}`,
|
|
991
|
+
`}`,
|
|
992
|
+
].join("\n");
|
|
993
|
+
const result = internalParse(expandedText, previousInstructions);
|
|
994
|
+
const bridgeInst = result.find((i) => i.kind === "bridge");
|
|
995
|
+
if (bridgeInst)
|
|
996
|
+
bridgeInst.passthrough = passthroughName;
|
|
997
|
+
return result;
|
|
998
|
+
}
|
|
999
|
+
// Full bridge block
|
|
1000
|
+
const bodyLines = subs(node, "bridgeBodyLine");
|
|
1001
|
+
const { handles, wires, arrayIterators, pipeHandles } = buildBridgeBody(bodyLines, typeName, fieldName, previousInstructions, 0);
|
|
1002
|
+
// Inline define invocations
|
|
1003
|
+
const instanceCounters = new Map();
|
|
1004
|
+
for (const hb of handles) {
|
|
1005
|
+
if (hb.kind !== "tool")
|
|
1006
|
+
continue;
|
|
1007
|
+
const name = hb.name;
|
|
1008
|
+
const lastDot = name.lastIndexOf(".");
|
|
1009
|
+
if (lastDot !== -1) {
|
|
1010
|
+
const key = `${name.substring(0, lastDot)}:${name.substring(lastDot + 1)}`;
|
|
1011
|
+
instanceCounters.set(key, (instanceCounters.get(key) ?? 0) + 1);
|
|
1012
|
+
}
|
|
1013
|
+
else {
|
|
1014
|
+
const key = `Tools:${name}`;
|
|
1015
|
+
instanceCounters.set(key, (instanceCounters.get(key) ?? 0) + 1);
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
const nextForkSeqRef = { value: pipeHandles.length > 0
|
|
1019
|
+
? Math.max(...pipeHandles.map(p => {
|
|
1020
|
+
const parts = p.key.split(":");
|
|
1021
|
+
return parseInt(parts[parts.length - 1]) || 0;
|
|
1022
|
+
}).filter(n => n >= 100000).map(n => n - 100000 + 1), 0)
|
|
1023
|
+
: 0
|
|
1024
|
+
};
|
|
1025
|
+
for (const hb of handles) {
|
|
1026
|
+
if (hb.kind !== "define")
|
|
1027
|
+
continue;
|
|
1028
|
+
const def = previousInstructions.find((inst) => inst.kind === "define" && inst.name === hb.name);
|
|
1029
|
+
if (!def) {
|
|
1030
|
+
throw new Error(`Define "${hb.name}" referenced by handle "${hb.handle}" not found`);
|
|
1031
|
+
}
|
|
1032
|
+
inlineDefine(hb.handle, def, typeName, fieldName, wires, pipeHandles, handles, instanceCounters, nextForkSeqRef);
|
|
1033
|
+
}
|
|
1034
|
+
const instructions = [];
|
|
1035
|
+
instructions.push({
|
|
1036
|
+
kind: "bridge",
|
|
1037
|
+
type: typeName,
|
|
1038
|
+
field: fieldName,
|
|
1039
|
+
handles,
|
|
1040
|
+
wires,
|
|
1041
|
+
arrayIterators: Object.keys(arrayIterators).length > 0 ? arrayIterators : undefined,
|
|
1042
|
+
pipeHandles: pipeHandles.length > 0 ? pipeHandles : undefined,
|
|
1043
|
+
});
|
|
1044
|
+
return instructions;
|
|
1045
|
+
}
|
|
1046
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1047
|
+
// Bridge/Define body builder
|
|
1048
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1049
|
+
function buildBridgeBody(bodyLines, bridgeType, bridgeField, previousInstructions, _lineOffset) {
|
|
1050
|
+
const handleRes = new Map();
|
|
1051
|
+
const handleBindings = [];
|
|
1052
|
+
const instanceCounters = new Map();
|
|
1053
|
+
const wires = [];
|
|
1054
|
+
const arrayIterators = {};
|
|
1055
|
+
let nextForkSeq = 0;
|
|
1056
|
+
const pipeHandleEntries = [];
|
|
1057
|
+
// ── Step 1: Process with-declarations ─────────────────────────────────
|
|
1058
|
+
for (const bodyLine of bodyLines) {
|
|
1059
|
+
const withNode = bodyLine.children.bridgeWithDecl?.[0];
|
|
1060
|
+
if (!withNode)
|
|
1061
|
+
continue;
|
|
1062
|
+
const wc = withNode.children;
|
|
1063
|
+
const lineNum = line(findFirstToken(withNode));
|
|
1064
|
+
const checkDuplicate = (handle) => {
|
|
1065
|
+
if (handleRes.has(handle)) {
|
|
1066
|
+
throw new Error(`Line ${lineNum}: Duplicate handle name "${handle}"`);
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
if (wc.inputKw) {
|
|
1070
|
+
const handle = wc.inputAlias ? extractNameToken(wc.inputAlias[0]) : "input";
|
|
1071
|
+
checkDuplicate(handle);
|
|
1072
|
+
handleBindings.push({ handle, kind: "input" });
|
|
1073
|
+
handleRes.set(handle, { module: SELF_MODULE, type: bridgeType, field: bridgeField });
|
|
1074
|
+
}
|
|
1075
|
+
else if (wc.outputKw) {
|
|
1076
|
+
const handle = wc.outputAlias ? extractNameToken(wc.outputAlias[0]) : "output";
|
|
1077
|
+
checkDuplicate(handle);
|
|
1078
|
+
handleBindings.push({ handle, kind: "output" });
|
|
1079
|
+
handleRes.set(handle, { module: SELF_MODULE, type: bridgeType, field: bridgeField });
|
|
1080
|
+
}
|
|
1081
|
+
else if (wc.contextKw) {
|
|
1082
|
+
const handle = wc.contextAlias ? extractNameToken(wc.contextAlias[0]) : "context";
|
|
1083
|
+
checkDuplicate(handle);
|
|
1084
|
+
handleBindings.push({ handle, kind: "context" });
|
|
1085
|
+
handleRes.set(handle, { module: SELF_MODULE, type: "Context", field: "context" });
|
|
1086
|
+
}
|
|
1087
|
+
else if (wc.constKw) {
|
|
1088
|
+
const handle = wc.constAlias ? extractNameToken(wc.constAlias[0]) : "const";
|
|
1089
|
+
checkDuplicate(handle);
|
|
1090
|
+
handleBindings.push({ handle, kind: "const" });
|
|
1091
|
+
handleRes.set(handle, { module: SELF_MODULE, type: "Const", field: "const" });
|
|
1092
|
+
}
|
|
1093
|
+
else if (wc.refName) {
|
|
1094
|
+
const name = extractDottedName(wc.refName[0]);
|
|
1095
|
+
const lastDot = name.lastIndexOf(".");
|
|
1096
|
+
const defaultHandle = lastDot !== -1 ? name.substring(lastDot + 1) : name;
|
|
1097
|
+
const handle = wc.refAlias
|
|
1098
|
+
? extractNameToken(wc.refAlias[0])
|
|
1099
|
+
: defaultHandle;
|
|
1100
|
+
checkDuplicate(handle);
|
|
1101
|
+
if (wc.refAlias)
|
|
1102
|
+
assertNotReserved(handle, lineNum, "handle alias");
|
|
1103
|
+
// Check if it's a define reference
|
|
1104
|
+
const defineDef = previousInstructions.find((inst) => inst.kind === "define" && inst.name === name);
|
|
1105
|
+
if (defineDef) {
|
|
1106
|
+
handleBindings.push({ handle, kind: "define", name });
|
|
1107
|
+
handleRes.set(handle, { module: `__define_${handle}`, type: bridgeType, field: bridgeField });
|
|
1108
|
+
}
|
|
1109
|
+
else if (lastDot !== -1) {
|
|
1110
|
+
const modulePart = name.substring(0, lastDot);
|
|
1111
|
+
const fieldPart = name.substring(lastDot + 1);
|
|
1112
|
+
const key = `${modulePart}:${fieldPart}`;
|
|
1113
|
+
const instance = (instanceCounters.get(key) ?? 0) + 1;
|
|
1114
|
+
instanceCounters.set(key, instance);
|
|
1115
|
+
handleBindings.push({ handle, kind: "tool", name });
|
|
1116
|
+
handleRes.set(handle, { module: modulePart, type: bridgeType, field: fieldPart, instance });
|
|
1117
|
+
}
|
|
1118
|
+
else {
|
|
1119
|
+
const key = `Tools:${name}`;
|
|
1120
|
+
const instance = (instanceCounters.get(key) ?? 0) + 1;
|
|
1121
|
+
instanceCounters.set(key, instance);
|
|
1122
|
+
handleBindings.push({ handle, kind: "tool", name });
|
|
1123
|
+
handleRes.set(handle, { module: SELF_MODULE, type: "Tools", field: name, instance });
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// ── Helper: resolve address ────────────────────────────────────────────
|
|
1128
|
+
function resolveAddress(root, segments, lineNum) {
|
|
1129
|
+
const resolution = handleRes.get(root);
|
|
1130
|
+
if (!resolution) {
|
|
1131
|
+
if (segments.length === 0) {
|
|
1132
|
+
throw new Error(`Line ${lineNum}: Undeclared reference "${root}". Add 'with output as o' for output fields, or 'with ${root}' for a tool.`);
|
|
1133
|
+
}
|
|
1134
|
+
throw new Error(`Line ${lineNum}: Undeclared handle "${root}". Add 'with ${root}' or 'with ${root} as ${root}' to the bridge header.`);
|
|
1135
|
+
}
|
|
1136
|
+
const ref = {
|
|
1137
|
+
module: resolution.module,
|
|
1138
|
+
type: resolution.type,
|
|
1139
|
+
field: resolution.field,
|
|
1140
|
+
path: [...segments],
|
|
1141
|
+
};
|
|
1142
|
+
if (resolution.instance != null)
|
|
1143
|
+
ref.instance = resolution.instance;
|
|
1144
|
+
return ref;
|
|
1145
|
+
}
|
|
1146
|
+
function assertNoTargetIndices(ref, lineNum) {
|
|
1147
|
+
if (ref.path.some(seg => /^\d+$/.test(seg))) {
|
|
1148
|
+
throw new Error(`Line ${lineNum}: Explicit array index in wire target is not supported. Use array mapping (\`[] as iter { }\`) instead.`);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
// ── Helper: build source expression ────────────────────────────────────
|
|
1152
|
+
function buildSourceExpr(sourceNode, lineNum, forceOnOutermost) {
|
|
1153
|
+
const headNode = sub(sourceNode, "head");
|
|
1154
|
+
const pipeNodes = subs(sourceNode, "pipeSegment");
|
|
1155
|
+
if (pipeNodes.length === 0) {
|
|
1156
|
+
const { root, segments } = extractAddressPath(headNode);
|
|
1157
|
+
return resolveAddress(root, segments, lineNum);
|
|
1158
|
+
}
|
|
1159
|
+
// Pipe chain: all parts in order [head, ...pipeSegments]
|
|
1160
|
+
// The LAST part is the actual data source; everything before is a pipe handle.
|
|
1161
|
+
const allParts = [headNode, ...pipeNodes];
|
|
1162
|
+
const actualSourceNode = allParts[allParts.length - 1];
|
|
1163
|
+
const pipeChainNodes = allParts.slice(0, -1);
|
|
1164
|
+
// Validate all pipe handles
|
|
1165
|
+
for (const pipeNode of pipeChainNodes) {
|
|
1166
|
+
const { root } = extractAddressPath(pipeNode);
|
|
1167
|
+
if (!handleRes.has(root)) {
|
|
1168
|
+
throw new Error(`Line ${lineNum}: Undeclared handle in pipe: "${root}". Add 'with <tool> as ${root}' to the bridge header.`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
const { root: srcRoot, segments: srcSegments } = extractAddressPath(actualSourceNode);
|
|
1172
|
+
let prevOutRef = resolveAddress(srcRoot, srcSegments, lineNum);
|
|
1173
|
+
// Process pipe handles right-to-left (innermost first)
|
|
1174
|
+
const reversed = [...pipeChainNodes].reverse();
|
|
1175
|
+
for (let idx = 0; idx < reversed.length; idx++) {
|
|
1176
|
+
const pNode = reversed[idx];
|
|
1177
|
+
const { root: handleName, segments: handleSegs } = extractAddressPath(pNode);
|
|
1178
|
+
const fieldName = handleSegs.length > 0 ? handleSegs.join(".") : "in";
|
|
1179
|
+
const res = handleRes.get(handleName);
|
|
1180
|
+
const forkInstance = 100000 + nextForkSeq++;
|
|
1181
|
+
const forkKey = `${res.module}:${res.type}:${res.field}:${forkInstance}`;
|
|
1182
|
+
pipeHandleEntries.push({
|
|
1183
|
+
key: forkKey,
|
|
1184
|
+
handle: handleName,
|
|
1185
|
+
baseTrunk: { module: res.module, type: res.type, field: res.field, instance: res.instance },
|
|
1186
|
+
});
|
|
1187
|
+
const forkInRef = {
|
|
1188
|
+
module: res.module, type: res.type, field: res.field,
|
|
1189
|
+
instance: forkInstance,
|
|
1190
|
+
path: parsePath(fieldName),
|
|
1191
|
+
};
|
|
1192
|
+
const forkRootRef = {
|
|
1193
|
+
module: res.module, type: res.type, field: res.field,
|
|
1194
|
+
instance: forkInstance,
|
|
1195
|
+
path: [],
|
|
1196
|
+
};
|
|
1197
|
+
const isOutermost = idx === reversed.length - 1;
|
|
1198
|
+
wires.push({
|
|
1199
|
+
from: prevOutRef,
|
|
1200
|
+
to: forkInRef,
|
|
1201
|
+
pipe: true,
|
|
1202
|
+
...(forceOnOutermost && isOutermost ? { force: true } : {}),
|
|
1203
|
+
});
|
|
1204
|
+
prevOutRef = forkRootRef;
|
|
1205
|
+
}
|
|
1206
|
+
return prevOutRef;
|
|
1207
|
+
}
|
|
1208
|
+
// ── Helper: extract coalesce alternative ───────────────────────────────
|
|
1209
|
+
function extractCoalesceAlt(altNode, lineNum) {
|
|
1210
|
+
const c = altNode.children;
|
|
1211
|
+
if (c.stringLit)
|
|
1212
|
+
return { literal: c.stringLit[0].image };
|
|
1213
|
+
if (c.numberLit)
|
|
1214
|
+
return { literal: c.numberLit[0].image };
|
|
1215
|
+
if (c.intLit)
|
|
1216
|
+
return { literal: c.intLit[0].image };
|
|
1217
|
+
if (c.trueLit)
|
|
1218
|
+
return { literal: "true" };
|
|
1219
|
+
if (c.falseLit)
|
|
1220
|
+
return { literal: "false" };
|
|
1221
|
+
if (c.nullLit)
|
|
1222
|
+
return { literal: "null" };
|
|
1223
|
+
if (c.objectLit)
|
|
1224
|
+
return { literal: reconstructJson(c.objectLit[0]) };
|
|
1225
|
+
if (c.sourceAlt) {
|
|
1226
|
+
const srcNode = c.sourceAlt[0];
|
|
1227
|
+
return { sourceRef: buildSourceExpr(srcNode, lineNum, false) };
|
|
1228
|
+
}
|
|
1229
|
+
throw new Error(`Line ${lineNum}: Invalid coalesce alternative`);
|
|
1230
|
+
}
|
|
1231
|
+
// ── Step 2: Process wire lines ─────────────────────────────────────────
|
|
1232
|
+
for (const bodyLine of bodyLines) {
|
|
1233
|
+
const c = bodyLine.children;
|
|
1234
|
+
if (c.bridgeWithDecl)
|
|
1235
|
+
continue; // already processed
|
|
1236
|
+
const wireNode = c.bridgeWire?.[0];
|
|
1237
|
+
if (!wireNode)
|
|
1238
|
+
continue;
|
|
1239
|
+
const wc = wireNode.children;
|
|
1240
|
+
const lineNum = line(findFirstToken(wireNode));
|
|
1241
|
+
// Parse target
|
|
1242
|
+
const { root: targetRoot, segments: targetSegs } = extractAddressPath(sub(wireNode, "target"));
|
|
1243
|
+
const toRef = resolveAddress(targetRoot, targetSegs, lineNum);
|
|
1244
|
+
assertNoTargetIndices(toRef, lineNum);
|
|
1245
|
+
// ── Constant wire: target = value ──
|
|
1246
|
+
if (wc.equalsOp) {
|
|
1247
|
+
const value = extractBareValue(sub(wireNode, "constValue"));
|
|
1248
|
+
wires.push({ value, to: toRef });
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
// ── Pull wire: target <-[!] source [modifiers] ──
|
|
1252
|
+
const force = !!wc.forceArrow;
|
|
1253
|
+
// Array mapping?
|
|
1254
|
+
const arrayMappingNode = wc.arrayMapping?.[0];
|
|
1255
|
+
if (arrayMappingNode) {
|
|
1256
|
+
const firstSourceNode = sub(wireNode, "firstSource");
|
|
1257
|
+
const srcRef = buildSourceExpr(firstSourceNode, lineNum, force);
|
|
1258
|
+
wires.push({ from: srcRef, to: toRef });
|
|
1259
|
+
const iterName = extractNameToken(sub(arrayMappingNode, "iterName"));
|
|
1260
|
+
assertNotReserved(iterName, lineNum, "iterator handle");
|
|
1261
|
+
const arrayToPath = toRef.path;
|
|
1262
|
+
arrayIterators[arrayToPath[0]] = iterName;
|
|
1263
|
+
// Process element lines
|
|
1264
|
+
for (const elemLine of subs(arrayMappingNode, "elementLine")) {
|
|
1265
|
+
const elemC = elemLine.children;
|
|
1266
|
+
const elemLineNum = line(findFirstToken(elemLine));
|
|
1267
|
+
const elemTargetPathStr = extractDottedPathStr(sub(elemLine, "elemTarget"));
|
|
1268
|
+
const elemToPath = [...arrayToPath, ...parsePath(elemTargetPathStr)];
|
|
1269
|
+
if (elemC.elemEquals) {
|
|
1270
|
+
const value = extractBareValue(sub(elemLine, "elemValue"));
|
|
1271
|
+
wires.push({
|
|
1272
|
+
value,
|
|
1273
|
+
to: {
|
|
1274
|
+
module: SELF_MODULE,
|
|
1275
|
+
type: bridgeType,
|
|
1276
|
+
field: bridgeField,
|
|
1277
|
+
element: true,
|
|
1278
|
+
path: elemToPath,
|
|
1279
|
+
},
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
else if (elemC.elemArrow) {
|
|
1283
|
+
const elemSourceNode = sub(elemLine, "elemSource");
|
|
1284
|
+
const elemToRef = {
|
|
1285
|
+
module: SELF_MODULE,
|
|
1286
|
+
type: bridgeType,
|
|
1287
|
+
field: bridgeField,
|
|
1288
|
+
path: elemToPath,
|
|
1289
|
+
};
|
|
1290
|
+
// Check if iterator-relative source
|
|
1291
|
+
const elemHeadNode = sub(elemSourceNode, "head");
|
|
1292
|
+
const elemPipeSegs = subs(elemSourceNode, "pipeSegment");
|
|
1293
|
+
const { root: elemSrcRoot, segments: elemSrcSegs } = extractAddressPath(elemHeadNode);
|
|
1294
|
+
const sourceParts = [];
|
|
1295
|
+
if (elemSrcRoot === iterName && elemPipeSegs.length === 0) {
|
|
1296
|
+
sourceParts.push({
|
|
1297
|
+
ref: {
|
|
1298
|
+
module: SELF_MODULE,
|
|
1299
|
+
type: bridgeType,
|
|
1300
|
+
field: bridgeField,
|
|
1301
|
+
element: true,
|
|
1302
|
+
path: elemSrcSegs,
|
|
1303
|
+
},
|
|
1304
|
+
isPipeFork: false,
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
else {
|
|
1308
|
+
const ref = buildSourceExpr(elemSourceNode, elemLineNum, false);
|
|
1309
|
+
const isPipeFork = ref.instance != null && ref.path.length === 0 && elemPipeSegs.length > 0;
|
|
1310
|
+
sourceParts.push({ ref, isPipeFork });
|
|
1311
|
+
}
|
|
1312
|
+
// || alternatives
|
|
1313
|
+
let nullFallback;
|
|
1314
|
+
for (const alt of subs(elemLine, "elemNullAlt")) {
|
|
1315
|
+
const altResult = extractCoalesceAlt(alt, elemLineNum);
|
|
1316
|
+
if ("literal" in altResult) {
|
|
1317
|
+
nullFallback = altResult.literal;
|
|
1318
|
+
}
|
|
1319
|
+
else {
|
|
1320
|
+
sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false });
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
// ?? fallback
|
|
1324
|
+
let fallback;
|
|
1325
|
+
let fallbackRef;
|
|
1326
|
+
let fallbackInternalWires = [];
|
|
1327
|
+
const errorAlt = sub(elemLine, "elemErrorAlt");
|
|
1328
|
+
if (errorAlt) {
|
|
1329
|
+
const preLen = wires.length;
|
|
1330
|
+
const altResult = extractCoalesceAlt(errorAlt, elemLineNum);
|
|
1331
|
+
if ("literal" in altResult) {
|
|
1332
|
+
fallback = altResult.literal;
|
|
1333
|
+
}
|
|
1334
|
+
else {
|
|
1335
|
+
fallbackRef = altResult.sourceRef;
|
|
1336
|
+
fallbackInternalWires = wires.splice(preLen);
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
// Emit wires
|
|
1340
|
+
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
1341
|
+
const { ref: fromRef, isPipeFork } = sourceParts[ci];
|
|
1342
|
+
const isLast = ci === sourceParts.length - 1;
|
|
1343
|
+
const lastAttrs = isLast
|
|
1344
|
+
? {
|
|
1345
|
+
...(nullFallback ? { nullFallback } : {}),
|
|
1346
|
+
...(fallback ? { fallback } : {}),
|
|
1347
|
+
...(fallbackRef ? { fallbackRef } : {}),
|
|
1348
|
+
}
|
|
1349
|
+
: {};
|
|
1350
|
+
if (isPipeFork) {
|
|
1351
|
+
wires.push({ from: fromRef, to: elemToRef, pipe: true, ...lastAttrs });
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
wires.push({ from: fromRef, to: elemToRef, ...lastAttrs });
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
wires.push(...fallbackInternalWires);
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
continue;
|
|
1361
|
+
}
|
|
1362
|
+
// ── Regular pull wire (non-array) ──
|
|
1363
|
+
const firstSourceNode = sub(wireNode, "firstSource");
|
|
1364
|
+
const sourceParts = [];
|
|
1365
|
+
const headAddr = sub(firstSourceNode, "head");
|
|
1366
|
+
const pipeSegs = subs(firstSourceNode, "pipeSegment");
|
|
1367
|
+
const firstRef = buildSourceExpr(firstSourceNode, lineNum, force);
|
|
1368
|
+
const isPipeFork = firstRef.instance != null && firstRef.path.length === 0 && pipeSegs.length > 0;
|
|
1369
|
+
sourceParts.push({ ref: firstRef, isPipeFork });
|
|
1370
|
+
let nullFallback;
|
|
1371
|
+
for (const alt of subs(wireNode, "nullAlt")) {
|
|
1372
|
+
const altResult = extractCoalesceAlt(alt, lineNum);
|
|
1373
|
+
if ("literal" in altResult) {
|
|
1374
|
+
nullFallback = altResult.literal;
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
sourceParts.push({ ref: altResult.sourceRef, isPipeFork: false });
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
let fallback;
|
|
1381
|
+
let fallbackRef;
|
|
1382
|
+
let fallbackInternalWires = [];
|
|
1383
|
+
const errorAlt = sub(wireNode, "errorAlt");
|
|
1384
|
+
if (errorAlt) {
|
|
1385
|
+
const preLen = wires.length;
|
|
1386
|
+
const altResult = extractCoalesceAlt(errorAlt, lineNum);
|
|
1387
|
+
if ("literal" in altResult) {
|
|
1388
|
+
fallback = altResult.literal;
|
|
1389
|
+
}
|
|
1390
|
+
else {
|
|
1391
|
+
fallbackRef = altResult.sourceRef;
|
|
1392
|
+
fallbackInternalWires = wires.splice(preLen);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
for (let ci = 0; ci < sourceParts.length; ci++) {
|
|
1396
|
+
const { ref: fromRef, isPipeFork: isPipe } = sourceParts[ci];
|
|
1397
|
+
const isFirst = ci === 0;
|
|
1398
|
+
const isLast = ci === sourceParts.length - 1;
|
|
1399
|
+
const lastAttrs = isLast
|
|
1400
|
+
? {
|
|
1401
|
+
...(nullFallback ? { nullFallback } : {}),
|
|
1402
|
+
...(fallback ? { fallback } : {}),
|
|
1403
|
+
...(fallbackRef ? { fallbackRef } : {}),
|
|
1404
|
+
}
|
|
1405
|
+
: {};
|
|
1406
|
+
if (isPipe) {
|
|
1407
|
+
wires.push({ from: fromRef, to: toRef, pipe: true, ...lastAttrs });
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
wires.push({
|
|
1411
|
+
from: fromRef,
|
|
1412
|
+
to: toRef,
|
|
1413
|
+
...(force && isFirst ? { force: true } : {}),
|
|
1414
|
+
...lastAttrs,
|
|
1415
|
+
});
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
wires.push(...fallbackInternalWires);
|
|
1419
|
+
}
|
|
1420
|
+
return { handles: handleBindings, wires, arrayIterators, pipeHandles: pipeHandleEntries };
|
|
1421
|
+
}
|
|
1422
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1423
|
+
// inlineDefine (matching the regex parser)
|
|
1424
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
1425
|
+
function inlineDefine(defineHandle, defineDef, bridgeType, bridgeField, wires, pipeHandleEntries, handleBindings, instanceCounters, nextForkSeqRef) {
|
|
1426
|
+
const genericModule = `__define_${defineHandle}`;
|
|
1427
|
+
const inModule = `__define_in_${defineHandle}`;
|
|
1428
|
+
const outModule = `__define_out_${defineHandle}`;
|
|
1429
|
+
const defType = "Define";
|
|
1430
|
+
const defField = defineDef.name;
|
|
1431
|
+
const defCounters = new Map();
|
|
1432
|
+
const trunkRemap = new Map();
|
|
1433
|
+
for (const hb of defineDef.handles) {
|
|
1434
|
+
if (hb.kind === "input" || hb.kind === "output" || hb.kind === "context" || hb.kind === "const")
|
|
1435
|
+
continue;
|
|
1436
|
+
if (hb.kind === "define")
|
|
1437
|
+
continue;
|
|
1438
|
+
const name = hb.kind === "tool" ? hb.name : "";
|
|
1439
|
+
if (!name)
|
|
1440
|
+
continue;
|
|
1441
|
+
const lastDot = name.lastIndexOf(".");
|
|
1442
|
+
let oldModule, oldType, oldField, instanceKey, bridgeKey;
|
|
1443
|
+
if (lastDot !== -1) {
|
|
1444
|
+
oldModule = name.substring(0, lastDot);
|
|
1445
|
+
oldType = defType;
|
|
1446
|
+
oldField = name.substring(lastDot + 1);
|
|
1447
|
+
instanceKey = `${oldModule}:${oldField}`;
|
|
1448
|
+
bridgeKey = instanceKey;
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
oldModule = SELF_MODULE;
|
|
1452
|
+
oldType = "Tools";
|
|
1453
|
+
oldField = name;
|
|
1454
|
+
instanceKey = `Tools:${name}`;
|
|
1455
|
+
bridgeKey = instanceKey;
|
|
1456
|
+
}
|
|
1457
|
+
const oldInstance = (defCounters.get(instanceKey) ?? 0) + 1;
|
|
1458
|
+
defCounters.set(instanceKey, oldInstance);
|
|
1459
|
+
const newInstance = (instanceCounters.get(bridgeKey) ?? 0) + 1;
|
|
1460
|
+
instanceCounters.set(bridgeKey, newInstance);
|
|
1461
|
+
const oldKey = `${oldModule}:${oldType}:${oldField}:${oldInstance}`;
|
|
1462
|
+
trunkRemap.set(oldKey, { module: oldModule, type: oldType, field: oldField, instance: newInstance });
|
|
1463
|
+
handleBindings.push({ handle: `${defineHandle}$${hb.handle}`, kind: "tool", name });
|
|
1464
|
+
}
|
|
1465
|
+
// Remap existing bridge wires pointing at the generic define module
|
|
1466
|
+
for (const wire of wires) {
|
|
1467
|
+
if ("from" in wire) {
|
|
1468
|
+
if (wire.to.module === genericModule)
|
|
1469
|
+
wire.to = { ...wire.to, module: inModule };
|
|
1470
|
+
if (wire.from.module === genericModule)
|
|
1471
|
+
wire.from = { ...wire.from, module: outModule };
|
|
1472
|
+
if (wire.fallbackRef?.module === genericModule)
|
|
1473
|
+
wire.fallbackRef = { ...wire.fallbackRef, module: outModule };
|
|
1474
|
+
}
|
|
1475
|
+
if ("value" in wire && wire.to.module === genericModule)
|
|
1476
|
+
wire.to = { ...wire.to, module: inModule };
|
|
1477
|
+
}
|
|
1478
|
+
const forkOffset = nextForkSeqRef.value;
|
|
1479
|
+
let maxDefForkSeq = 0;
|
|
1480
|
+
function remapRef(ref, side) {
|
|
1481
|
+
if (ref.module === SELF_MODULE && ref.type === defType && ref.field === defField) {
|
|
1482
|
+
const targetModule = side === "from" ? inModule : outModule;
|
|
1483
|
+
return { ...ref, module: targetModule, type: bridgeType, field: bridgeField };
|
|
1484
|
+
}
|
|
1485
|
+
const key = `${ref.module}:${ref.type}:${ref.field}:${ref.instance ?? ""}`;
|
|
1486
|
+
const newTrunk = trunkRemap.get(key);
|
|
1487
|
+
if (newTrunk)
|
|
1488
|
+
return { ...ref, module: newTrunk.module, type: newTrunk.type, field: newTrunk.field, instance: newTrunk.instance };
|
|
1489
|
+
if (ref.instance != null && ref.instance >= 100000) {
|
|
1490
|
+
const defSeq = ref.instance - 100000;
|
|
1491
|
+
if (defSeq + 1 > maxDefForkSeq)
|
|
1492
|
+
maxDefForkSeq = defSeq + 1;
|
|
1493
|
+
return { ...ref, instance: ref.instance + forkOffset };
|
|
1494
|
+
}
|
|
1495
|
+
return ref;
|
|
1496
|
+
}
|
|
1497
|
+
for (const wire of defineDef.wires) {
|
|
1498
|
+
const cloned = JSON.parse(JSON.stringify(wire));
|
|
1499
|
+
if ("from" in cloned) {
|
|
1500
|
+
cloned.from = remapRef(cloned.from, "from");
|
|
1501
|
+
cloned.to = remapRef(cloned.to, "to");
|
|
1502
|
+
if (cloned.fallbackRef)
|
|
1503
|
+
cloned.fallbackRef = remapRef(cloned.fallbackRef, "from");
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
cloned.to = remapRef(cloned.to, "to");
|
|
1507
|
+
}
|
|
1508
|
+
wires.push(cloned);
|
|
1509
|
+
}
|
|
1510
|
+
nextForkSeqRef.value += maxDefForkSeq;
|
|
1511
|
+
if (defineDef.pipeHandles) {
|
|
1512
|
+
for (const ph of defineDef.pipeHandles) {
|
|
1513
|
+
const parts = ph.key.split(":");
|
|
1514
|
+
const phInstance = parseInt(parts[parts.length - 1]);
|
|
1515
|
+
let newKey = ph.key;
|
|
1516
|
+
if (phInstance >= 100000) {
|
|
1517
|
+
const newInst = phInstance + forkOffset;
|
|
1518
|
+
parts[parts.length - 1] = String(newInst);
|
|
1519
|
+
newKey = parts.join(":");
|
|
1520
|
+
}
|
|
1521
|
+
const bt = ph.baseTrunk;
|
|
1522
|
+
const btKey = `${bt.module}:${defType}:${bt.field}:${bt.instance ?? ""}`;
|
|
1523
|
+
const newBt = trunkRemap.get(btKey);
|
|
1524
|
+
const btKey2 = `${bt.module}:Tools:${bt.field}:${bt.instance ?? ""}`;
|
|
1525
|
+
const newBt2 = trunkRemap.get(btKey2);
|
|
1526
|
+
const resolvedBt = newBt ?? newBt2;
|
|
1527
|
+
pipeHandleEntries.push({
|
|
1528
|
+
key: newKey,
|
|
1529
|
+
handle: `${defineHandle}$${ph.handle}`,
|
|
1530
|
+
baseTrunk: resolvedBt
|
|
1531
|
+
? { module: resolvedBt.module, type: resolvedBt.type, field: resolvedBt.field, instance: resolvedBt.instance }
|
|
1532
|
+
: ph.baseTrunk,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|