@gabrielbryk/jq-ts 1.1.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/LICENSE +21 -0
- package/README.md +58 -0
- package/dist/index.cjs +2040 -0
- package/dist/index.d.cts +324 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +324 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +2031 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +102 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,2040 @@
|
|
|
1
|
+
|
|
2
|
+
//#region src/errors.ts
|
|
3
|
+
var BaseError = class extends Error {
|
|
4
|
+
kind;
|
|
5
|
+
span;
|
|
6
|
+
constructor(kind, message, span) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.kind = kind;
|
|
9
|
+
this.span = span;
|
|
10
|
+
this.name = `${kind.charAt(0).toUpperCase() + kind.slice(1)}Error`;
|
|
11
|
+
}
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Thrown when the lexer encounters invalid input characters.
|
|
15
|
+
*/
|
|
16
|
+
var LexError = class extends BaseError {
|
|
17
|
+
constructor(message, span) {
|
|
18
|
+
super("lex", message, span);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Thrown when the parser encounters invalid syntax.
|
|
23
|
+
*/
|
|
24
|
+
var ParseError = class extends BaseError {
|
|
25
|
+
constructor(message, span) {
|
|
26
|
+
super("parse", message, span);
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Thrown when the AST contains unsupported or invalid features (e.g., restricted operators).
|
|
31
|
+
*/
|
|
32
|
+
var ValidationError = class extends BaseError {
|
|
33
|
+
constructor(message, span) {
|
|
34
|
+
super("validate", message, span);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Thrown during execution for runtime issues (type errors, limits exceeded, user-raised errors).
|
|
39
|
+
*/
|
|
40
|
+
var RuntimeError = class extends BaseError {
|
|
41
|
+
constructor(message, span) {
|
|
42
|
+
super("runtime", message, span);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
//#endregion
|
|
47
|
+
//#region src/tokens.ts
|
|
48
|
+
const keywordKinds = {
|
|
49
|
+
if: "If",
|
|
50
|
+
then: "Then",
|
|
51
|
+
elif: "Elif",
|
|
52
|
+
else: "Else",
|
|
53
|
+
end: "End",
|
|
54
|
+
as: "As",
|
|
55
|
+
and: "And",
|
|
56
|
+
or: "Or",
|
|
57
|
+
not: "Not",
|
|
58
|
+
null: "Null",
|
|
59
|
+
true: "True",
|
|
60
|
+
false: "False",
|
|
61
|
+
reduce: "Reduce",
|
|
62
|
+
foreach: "Foreach",
|
|
63
|
+
try: "Try",
|
|
64
|
+
catch: "Catch"
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
//#endregion
|
|
68
|
+
//#region src/lexer.ts
|
|
69
|
+
const lex = (text) => {
|
|
70
|
+
const tokens = [];
|
|
71
|
+
const length = text.length;
|
|
72
|
+
let pos = 0;
|
|
73
|
+
const peek = (offset = 0) => text[pos + offset];
|
|
74
|
+
const advance = () => text[pos++];
|
|
75
|
+
const makeSpan = (start, end) => ({
|
|
76
|
+
start,
|
|
77
|
+
end
|
|
78
|
+
});
|
|
79
|
+
const pushToken = (kind, start, end, value) => {
|
|
80
|
+
tokens.push({
|
|
81
|
+
kind,
|
|
82
|
+
span: makeSpan(start, end),
|
|
83
|
+
value
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
const isWhitespace = (ch) => ch === " " || ch === " " || ch === "\n" || ch === "\r";
|
|
87
|
+
const isDigit = (ch) => !!ch && ch >= "0" && ch <= "9";
|
|
88
|
+
const isIdentifierStart = (ch) => !!ch && (ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_");
|
|
89
|
+
const isIdentifierPart = (ch) => isIdentifierStart(ch) || isDigit(ch);
|
|
90
|
+
while (pos < length) {
|
|
91
|
+
const ch = peek();
|
|
92
|
+
if (isWhitespace(ch)) {
|
|
93
|
+
advance();
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (ch === "#") {
|
|
97
|
+
while (pos < length && peek() !== "\n") advance();
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
const start = pos;
|
|
101
|
+
switch (ch) {
|
|
102
|
+
case "\"": {
|
|
103
|
+
const endPos = readString(start);
|
|
104
|
+
pushToken("String", start, endPos, readStringValue(start, start + 1, endPos - 1));
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
case ".":
|
|
108
|
+
advance();
|
|
109
|
+
if (peek() === ".") {
|
|
110
|
+
advance();
|
|
111
|
+
pushToken("DotDot", start, pos);
|
|
112
|
+
} else pushToken("Dot", start, pos);
|
|
113
|
+
continue;
|
|
114
|
+
case ";":
|
|
115
|
+
advance();
|
|
116
|
+
pushToken("Semicolon", start, pos);
|
|
117
|
+
continue;
|
|
118
|
+
case ",":
|
|
119
|
+
advance();
|
|
120
|
+
pushToken("Comma", start, pos);
|
|
121
|
+
continue;
|
|
122
|
+
case "|":
|
|
123
|
+
advance();
|
|
124
|
+
pushToken("Pipe", start, pos);
|
|
125
|
+
continue;
|
|
126
|
+
case "/":
|
|
127
|
+
advance();
|
|
128
|
+
if (peek() === "/") {
|
|
129
|
+
advance();
|
|
130
|
+
pushToken("Alt", start, pos);
|
|
131
|
+
} else pushToken("Slash", start, pos);
|
|
132
|
+
continue;
|
|
133
|
+
case "(":
|
|
134
|
+
advance();
|
|
135
|
+
pushToken("LParen", start, pos);
|
|
136
|
+
continue;
|
|
137
|
+
case ")":
|
|
138
|
+
advance();
|
|
139
|
+
pushToken("RParen", start, pos);
|
|
140
|
+
continue;
|
|
141
|
+
case "[":
|
|
142
|
+
advance();
|
|
143
|
+
pushToken("LBracket", start, pos);
|
|
144
|
+
continue;
|
|
145
|
+
case "]":
|
|
146
|
+
advance();
|
|
147
|
+
pushToken("RBracket", start, pos);
|
|
148
|
+
continue;
|
|
149
|
+
case "{":
|
|
150
|
+
advance();
|
|
151
|
+
pushToken("LBrace", start, pos);
|
|
152
|
+
continue;
|
|
153
|
+
case "}":
|
|
154
|
+
advance();
|
|
155
|
+
pushToken("RBrace", start, pos);
|
|
156
|
+
continue;
|
|
157
|
+
case ":":
|
|
158
|
+
advance();
|
|
159
|
+
pushToken("Colon", start, pos);
|
|
160
|
+
continue;
|
|
161
|
+
case "+":
|
|
162
|
+
advance();
|
|
163
|
+
pushToken("Plus", start, pos);
|
|
164
|
+
continue;
|
|
165
|
+
case "-":
|
|
166
|
+
advance();
|
|
167
|
+
pushToken("Minus", start, pos);
|
|
168
|
+
continue;
|
|
169
|
+
case "*":
|
|
170
|
+
advance();
|
|
171
|
+
pushToken("Star", start, pos);
|
|
172
|
+
continue;
|
|
173
|
+
case "%":
|
|
174
|
+
advance();
|
|
175
|
+
pushToken("Percent", start, pos);
|
|
176
|
+
continue;
|
|
177
|
+
case "=":
|
|
178
|
+
advance();
|
|
179
|
+
if (peek() === "=") {
|
|
180
|
+
advance();
|
|
181
|
+
pushToken("EqualEqual", start, pos);
|
|
182
|
+
} else throw new LexError("Unexpected \"=\" (only \"==\" supported)", makeSpan(start, pos));
|
|
183
|
+
continue;
|
|
184
|
+
case "!":
|
|
185
|
+
advance();
|
|
186
|
+
if (peek() === "=") {
|
|
187
|
+
advance();
|
|
188
|
+
pushToken("BangEqual", start, pos);
|
|
189
|
+
} else throw new LexError("Unexpected \"!\" (only \"!=\" supported)", makeSpan(start, pos));
|
|
190
|
+
continue;
|
|
191
|
+
case "<":
|
|
192
|
+
advance();
|
|
193
|
+
if (peek() === "=") {
|
|
194
|
+
advance();
|
|
195
|
+
pushToken("LessEqual", start, pos);
|
|
196
|
+
} else pushToken("Less", start, pos);
|
|
197
|
+
continue;
|
|
198
|
+
case ">":
|
|
199
|
+
advance();
|
|
200
|
+
if (peek() === "=") {
|
|
201
|
+
advance();
|
|
202
|
+
pushToken("GreaterEqual", start, pos);
|
|
203
|
+
} else pushToken("Greater", start, pos);
|
|
204
|
+
continue;
|
|
205
|
+
case "$": {
|
|
206
|
+
advance();
|
|
207
|
+
if (!isIdentifierStart(peek())) throw new LexError("Expected identifier after \"$\"", makeSpan(start, pos));
|
|
208
|
+
const nameStart = pos;
|
|
209
|
+
while (isIdentifierPart(peek())) advance();
|
|
210
|
+
pushToken("Variable", start, pos, text.slice(nameStart, pos));
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
default: break;
|
|
214
|
+
}
|
|
215
|
+
if (isDigit(ch)) {
|
|
216
|
+
const end = readNumber(start);
|
|
217
|
+
const raw = text.slice(start, end);
|
|
218
|
+
const value = Number(raw);
|
|
219
|
+
if (!Number.isFinite(value)) throw new LexError(`Invalid number literal: ${raw}`, makeSpan(start, end));
|
|
220
|
+
pushToken("Number", start, end, value);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (isIdentifierStart(ch)) {
|
|
224
|
+
while (isIdentifierPart(peek())) advance();
|
|
225
|
+
const raw = text.slice(start, pos);
|
|
226
|
+
const keyword = keywordKinds[raw];
|
|
227
|
+
if (keyword === "Null" || keyword === "True" || keyword === "False") pushToken(keyword, start, pos);
|
|
228
|
+
else if (keyword) pushToken(keyword, start, pos);
|
|
229
|
+
else pushToken("Identifier", start, pos, raw);
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
throw new LexError(`Unexpected character "${ch}"`, makeSpan(start, start + 1));
|
|
233
|
+
}
|
|
234
|
+
pushToken("EOF", pos, pos);
|
|
235
|
+
return tokens;
|
|
236
|
+
function readNumber(tokenStart) {
|
|
237
|
+
while (isDigit(peek())) advance();
|
|
238
|
+
if (peek() === "." && isDigit(peek(1))) {
|
|
239
|
+
advance();
|
|
240
|
+
while (isDigit(peek())) advance();
|
|
241
|
+
}
|
|
242
|
+
if (peek() === "e" || peek() === "E") {
|
|
243
|
+
advance();
|
|
244
|
+
if (peek() === "+" || peek() === "-") advance();
|
|
245
|
+
if (!isDigit(peek())) throw new LexError(`Invalid exponent in number literal`, makeSpan(tokenStart, pos));
|
|
246
|
+
while (isDigit(peek())) advance();
|
|
247
|
+
}
|
|
248
|
+
return pos;
|
|
249
|
+
}
|
|
250
|
+
function readString(tokenStart) {
|
|
251
|
+
advance();
|
|
252
|
+
while (pos < length) {
|
|
253
|
+
const current = advance();
|
|
254
|
+
if (current === "\"") return pos;
|
|
255
|
+
if (current === "\\") {
|
|
256
|
+
const esc = advance();
|
|
257
|
+
if (!esc) break;
|
|
258
|
+
if ("\"\\/bfnrt".includes(esc)) continue;
|
|
259
|
+
if (esc === "u") {
|
|
260
|
+
for (let i = 0; i < 4; i += 1) {
|
|
261
|
+
const hex = advance();
|
|
262
|
+
if (!hex || !isHexDigit(hex)) throw new LexError("Invalid Unicode escape in string literal", makeSpan(tokenStart, pos));
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
throw new LexError(`Invalid escape sequence "\\${esc}"`, makeSpan(tokenStart, pos));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
throw new LexError("Unterminated string literal", makeSpan(tokenStart, pos));
|
|
270
|
+
}
|
|
271
|
+
function readStringValue(tokenStart, innerStart, innerEnd) {
|
|
272
|
+
let result = "";
|
|
273
|
+
let i = innerStart;
|
|
274
|
+
while (i < innerEnd) {
|
|
275
|
+
const ch = text[i];
|
|
276
|
+
if (ch === "\\") {
|
|
277
|
+
const next = text[i + 1];
|
|
278
|
+
switch (next) {
|
|
279
|
+
case "\"":
|
|
280
|
+
case "\\":
|
|
281
|
+
case "/":
|
|
282
|
+
result += next;
|
|
283
|
+
i += 2;
|
|
284
|
+
break;
|
|
285
|
+
case "b":
|
|
286
|
+
result += "\b";
|
|
287
|
+
i += 2;
|
|
288
|
+
break;
|
|
289
|
+
case "f":
|
|
290
|
+
result += "\f";
|
|
291
|
+
i += 2;
|
|
292
|
+
break;
|
|
293
|
+
case "n":
|
|
294
|
+
result += "\n";
|
|
295
|
+
i += 2;
|
|
296
|
+
break;
|
|
297
|
+
case "r":
|
|
298
|
+
result += "\r";
|
|
299
|
+
i += 2;
|
|
300
|
+
break;
|
|
301
|
+
case "t":
|
|
302
|
+
result += " ";
|
|
303
|
+
i += 2;
|
|
304
|
+
break;
|
|
305
|
+
case "u": {
|
|
306
|
+
const hex = text.slice(i + 2, i + 6);
|
|
307
|
+
result += String.fromCharCode(Number.parseInt(hex, 16));
|
|
308
|
+
i += 6;
|
|
309
|
+
break;
|
|
310
|
+
}
|
|
311
|
+
default: throw new LexError(`Invalid escape sequence "\\${next}"`, makeSpan(tokenStart, tokenStart + (i - innerStart) + 2));
|
|
312
|
+
}
|
|
313
|
+
} else {
|
|
314
|
+
result += ch;
|
|
315
|
+
i += 1;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return result;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
const isHexDigit = (ch) => !!ch && (ch >= "0" && ch <= "9" || ch >= "a" && ch <= "f" || ch >= "A" && ch <= "F");
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/parser.ts
|
|
325
|
+
/**
|
|
326
|
+
* Parses a jq source string into an Abstract Syntax Tree (AST).
|
|
327
|
+
*
|
|
328
|
+
* @param source - The input jq query string.
|
|
329
|
+
* @returns The root {@link FilterNode} of the AST.
|
|
330
|
+
* @throws {LexError} If the source contains invalid characters.
|
|
331
|
+
* @throws {ParseError} If the syntax is invalid.
|
|
332
|
+
*/
|
|
333
|
+
const parse = (source) => {
|
|
334
|
+
return new Parser(lex(source)).parseFilter();
|
|
335
|
+
};
|
|
336
|
+
var Parser = class {
|
|
337
|
+
current = 0;
|
|
338
|
+
constructor(tokens) {
|
|
339
|
+
this.tokens = tokens;
|
|
340
|
+
}
|
|
341
|
+
parseFilter() {
|
|
342
|
+
const expr = this.parseBinding();
|
|
343
|
+
this.consume("EOF", "Expected end of expression");
|
|
344
|
+
return expr;
|
|
345
|
+
}
|
|
346
|
+
parseBinding() {
|
|
347
|
+
let expr = this.parseComma();
|
|
348
|
+
while (this.match("As")) {
|
|
349
|
+
const varToken = this.consume("Variable", "Expected variable name after \"as\"");
|
|
350
|
+
this.consume("Pipe", "Expected \"|\" after variable binding");
|
|
351
|
+
const body = this.parseBinding();
|
|
352
|
+
expr = {
|
|
353
|
+
kind: "As",
|
|
354
|
+
bind: expr,
|
|
355
|
+
name: String(varToken.value),
|
|
356
|
+
body,
|
|
357
|
+
span: spanBetween(expr.span, body.span)
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
return expr;
|
|
361
|
+
}
|
|
362
|
+
parseComma() {
|
|
363
|
+
let expr = this.parsePipe();
|
|
364
|
+
while (this.match("Comma")) {
|
|
365
|
+
const right = this.parsePipe();
|
|
366
|
+
expr = {
|
|
367
|
+
kind: "Comma",
|
|
368
|
+
left: expr,
|
|
369
|
+
right,
|
|
370
|
+
span: spanBetween(expr.span, right.span)
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
return expr;
|
|
374
|
+
}
|
|
375
|
+
parsePipe() {
|
|
376
|
+
let expr = this.parsePipeOperand();
|
|
377
|
+
while (this.match("Pipe")) {
|
|
378
|
+
const right = this.parsePipeOperand();
|
|
379
|
+
expr = {
|
|
380
|
+
kind: "Pipe",
|
|
381
|
+
left: expr,
|
|
382
|
+
right,
|
|
383
|
+
span: spanBetween(expr.span, right.span)
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
return expr;
|
|
387
|
+
}
|
|
388
|
+
parsePipeOperand() {
|
|
389
|
+
return this.parseAlt();
|
|
390
|
+
}
|
|
391
|
+
parseAlt() {
|
|
392
|
+
let expr = this.parseOr();
|
|
393
|
+
while (this.match("Alt")) {
|
|
394
|
+
const right = this.parseOr();
|
|
395
|
+
expr = {
|
|
396
|
+
kind: "Alt",
|
|
397
|
+
left: expr,
|
|
398
|
+
right,
|
|
399
|
+
span: spanBetween(expr.span, right.span)
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
return expr;
|
|
403
|
+
}
|
|
404
|
+
parseOr() {
|
|
405
|
+
let expr = this.parseAnd();
|
|
406
|
+
while (this.match("Or")) {
|
|
407
|
+
const right = this.parseAnd();
|
|
408
|
+
expr = {
|
|
409
|
+
kind: "Bool",
|
|
410
|
+
op: "Or",
|
|
411
|
+
left: expr,
|
|
412
|
+
right,
|
|
413
|
+
span: spanBetween(expr.span, right.span)
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
return expr;
|
|
417
|
+
}
|
|
418
|
+
parseAnd() {
|
|
419
|
+
let expr = this.parseComparison();
|
|
420
|
+
while (this.match("And")) {
|
|
421
|
+
const right = this.parseComparison();
|
|
422
|
+
expr = {
|
|
423
|
+
kind: "Bool",
|
|
424
|
+
op: "And",
|
|
425
|
+
left: expr,
|
|
426
|
+
right,
|
|
427
|
+
span: spanBetween(expr.span, right.span)
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
return expr;
|
|
431
|
+
}
|
|
432
|
+
parseComparison() {
|
|
433
|
+
let expr = this.parseAdd();
|
|
434
|
+
while (true) if (this.match("EqualEqual")) {
|
|
435
|
+
const right = this.parseAdd();
|
|
436
|
+
expr = this.makeBinary(expr, right, "Eq");
|
|
437
|
+
} else if (this.match("BangEqual")) {
|
|
438
|
+
const right = this.parseAdd();
|
|
439
|
+
expr = this.makeBinary(expr, right, "Neq");
|
|
440
|
+
} else if (this.match("Less")) {
|
|
441
|
+
const right = this.parseAdd();
|
|
442
|
+
expr = this.makeBinary(expr, right, "Lt");
|
|
443
|
+
} else if (this.match("LessEqual")) {
|
|
444
|
+
const right = this.parseAdd();
|
|
445
|
+
expr = this.makeBinary(expr, right, "Lte");
|
|
446
|
+
} else if (this.match("Greater")) {
|
|
447
|
+
const right = this.parseAdd();
|
|
448
|
+
expr = this.makeBinary(expr, right, "Gt");
|
|
449
|
+
} else if (this.match("GreaterEqual")) {
|
|
450
|
+
const right = this.parseAdd();
|
|
451
|
+
expr = this.makeBinary(expr, right, "Gte");
|
|
452
|
+
} else break;
|
|
453
|
+
return expr;
|
|
454
|
+
}
|
|
455
|
+
parseAdd() {
|
|
456
|
+
let expr = this.parseMul();
|
|
457
|
+
while (true) if (this.match("Plus")) {
|
|
458
|
+
const right = this.parseMul();
|
|
459
|
+
expr = this.makeBinary(expr, right, "+");
|
|
460
|
+
} else if (this.match("Minus")) {
|
|
461
|
+
const right = this.parseMul();
|
|
462
|
+
expr = this.makeBinary(expr, right, "-");
|
|
463
|
+
} else break;
|
|
464
|
+
return expr;
|
|
465
|
+
}
|
|
466
|
+
parseMul() {
|
|
467
|
+
let expr = this.parseUnary();
|
|
468
|
+
while (true) if (this.match("Star")) {
|
|
469
|
+
const right = this.parseUnary();
|
|
470
|
+
expr = this.makeBinary(expr, right, "*");
|
|
471
|
+
} else if (this.match("Slash")) {
|
|
472
|
+
const right = this.parseUnary();
|
|
473
|
+
expr = this.makeBinary(expr, right, "/");
|
|
474
|
+
} else if (this.match("Percent")) {
|
|
475
|
+
const right = this.parseUnary();
|
|
476
|
+
expr = this.makeBinary(expr, right, "%");
|
|
477
|
+
} else break;
|
|
478
|
+
return expr;
|
|
479
|
+
}
|
|
480
|
+
parseUnary() {
|
|
481
|
+
if (this.match("Not")) {
|
|
482
|
+
const op = this.previous();
|
|
483
|
+
const expr = this.parseUnary();
|
|
484
|
+
return {
|
|
485
|
+
kind: "Unary",
|
|
486
|
+
op: "Not",
|
|
487
|
+
expr,
|
|
488
|
+
span: spanBetween(op.span, expr.span)
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
if (this.match("Minus")) {
|
|
492
|
+
const op = this.previous();
|
|
493
|
+
const expr = this.parseUnary();
|
|
494
|
+
return {
|
|
495
|
+
kind: "Unary",
|
|
496
|
+
op: "Neg",
|
|
497
|
+
expr,
|
|
498
|
+
span: spanBetween(op.span, expr.span)
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
return this.parsePostfix();
|
|
502
|
+
}
|
|
503
|
+
parsePostfix() {
|
|
504
|
+
let expr = this.parsePrimary();
|
|
505
|
+
while (true) {
|
|
506
|
+
if (this.match("Dot")) {
|
|
507
|
+
expr = this.finishFieldAccess(expr);
|
|
508
|
+
continue;
|
|
509
|
+
}
|
|
510
|
+
if (this.match("LBracket")) {
|
|
511
|
+
if (this.match("RBracket")) {
|
|
512
|
+
const close = this.previous();
|
|
513
|
+
expr = {
|
|
514
|
+
kind: "Iterate",
|
|
515
|
+
target: expr,
|
|
516
|
+
span: spanBetween(expr.span, close.span)
|
|
517
|
+
};
|
|
518
|
+
continue;
|
|
519
|
+
}
|
|
520
|
+
const index = this.parsePipe();
|
|
521
|
+
const closing = this.consume("RBracket", "Expected \"]\" after index expression");
|
|
522
|
+
expr = {
|
|
523
|
+
kind: "IndexAccess",
|
|
524
|
+
target: expr,
|
|
525
|
+
index,
|
|
526
|
+
span: spanBetween(expr.span, closing.span)
|
|
527
|
+
};
|
|
528
|
+
continue;
|
|
529
|
+
}
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
return expr;
|
|
533
|
+
}
|
|
534
|
+
parsePrimary() {
|
|
535
|
+
if (this.match("Dot")) return this.parseLeadingDot(this.previous());
|
|
536
|
+
if (this.match("Null")) return this.literalNode(null, this.previous().span);
|
|
537
|
+
if (this.match("True")) return this.literalNode(true, this.previous().span);
|
|
538
|
+
if (this.match("False")) return this.literalNode(false, this.previous().span);
|
|
539
|
+
if (this.match("Number")) return this.literalNode(Number(this.previous().value), this.previous().span);
|
|
540
|
+
if (this.match("String")) return this.literalNode(String(this.previous().value), this.previous().span);
|
|
541
|
+
if (this.match("Variable")) {
|
|
542
|
+
const token = this.previous();
|
|
543
|
+
return {
|
|
544
|
+
kind: "Var",
|
|
545
|
+
name: String(token.value),
|
|
546
|
+
span: token.span
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (this.match("Identifier")) return this.finishIdentifier(this.previous());
|
|
550
|
+
if (this.match("If")) return this.parseIf(this.previous());
|
|
551
|
+
if (this.match("LParen")) {
|
|
552
|
+
const start = this.previous();
|
|
553
|
+
const expr = this.parseComma();
|
|
554
|
+
const close = this.consume("RParen", "Expected \")\" after expression");
|
|
555
|
+
expr.span = spanBetween(start.span, close.span);
|
|
556
|
+
return expr;
|
|
557
|
+
}
|
|
558
|
+
if (this.match("LBracket")) return this.parseArray(this.previous());
|
|
559
|
+
if (this.match("LBrace")) return this.parseObject(this.previous());
|
|
560
|
+
if (this.match("Reduce")) return this.parseReduce(this.previous());
|
|
561
|
+
if (this.match("Foreach")) return this.parseForeach(this.previous());
|
|
562
|
+
if (this.match("Try")) return this.parseTry(this.previous());
|
|
563
|
+
if (this.match("DotDot")) return {
|
|
564
|
+
kind: "Recurse",
|
|
565
|
+
span: this.previous().span
|
|
566
|
+
};
|
|
567
|
+
throw this.error(this.peek(), "Unexpected token");
|
|
568
|
+
}
|
|
569
|
+
parseReduce(start) {
|
|
570
|
+
const source = this.parsePipe();
|
|
571
|
+
this.consume("As", "Expected \"as\" after reduce source");
|
|
572
|
+
const varToken = this.consume("Variable", "Expected variable after \"as\"");
|
|
573
|
+
this.consume("LParen", "Expected \"(\" after variable");
|
|
574
|
+
const init = this.parseComma();
|
|
575
|
+
this.consume("Semicolon", "Expected \";\" after init");
|
|
576
|
+
const update = this.parseComma();
|
|
577
|
+
const end = this.consume("RParen", "Expected \")\" after update");
|
|
578
|
+
return {
|
|
579
|
+
kind: "Reduce",
|
|
580
|
+
source,
|
|
581
|
+
var: String(varToken.value),
|
|
582
|
+
init,
|
|
583
|
+
update,
|
|
584
|
+
span: spanBetween(start.span, end.span)
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
parseForeach(start) {
|
|
588
|
+
const source = this.parsePipe();
|
|
589
|
+
this.consume("As", "Expected \"as\" after foreach source");
|
|
590
|
+
const varToken = this.consume("Variable", "Expected variable after \"as\"");
|
|
591
|
+
this.consume("LParen", "Expected \"(\" after variable");
|
|
592
|
+
const init = this.parseComma();
|
|
593
|
+
this.consume("Semicolon", "Expected \";\" after init");
|
|
594
|
+
const update = this.parseComma();
|
|
595
|
+
let extract;
|
|
596
|
+
if (this.match("Semicolon")) extract = this.parseComma();
|
|
597
|
+
const end = this.consume("RParen", "Expected \")\" after foreach body");
|
|
598
|
+
return {
|
|
599
|
+
kind: "Foreach",
|
|
600
|
+
source,
|
|
601
|
+
var: String(varToken.value),
|
|
602
|
+
init,
|
|
603
|
+
update,
|
|
604
|
+
extract,
|
|
605
|
+
span: spanBetween(start.span, end.span)
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
parseTry(start) {
|
|
609
|
+
const body = this.parseComma();
|
|
610
|
+
let handler;
|
|
611
|
+
let endSpan = body.span;
|
|
612
|
+
if (this.match("Catch")) {
|
|
613
|
+
handler = this.parseComma();
|
|
614
|
+
endSpan = handler.span;
|
|
615
|
+
}
|
|
616
|
+
return {
|
|
617
|
+
kind: "Try",
|
|
618
|
+
body,
|
|
619
|
+
handler,
|
|
620
|
+
span: spanBetween(start.span, endSpan)
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
parseIf(start) {
|
|
624
|
+
const branches = [];
|
|
625
|
+
const firstCond = this.parsePipe();
|
|
626
|
+
this.consume("Then", "Expected \"then\" after condition");
|
|
627
|
+
const firstThen = this.parsePipe();
|
|
628
|
+
branches.push({
|
|
629
|
+
cond: firstCond,
|
|
630
|
+
then: firstThen
|
|
631
|
+
});
|
|
632
|
+
while (this.match("Elif")) {
|
|
633
|
+
const cond = this.parsePipe();
|
|
634
|
+
this.consume("Then", "Expected \"then\" after elif condition");
|
|
635
|
+
const thenBranch = this.parsePipe();
|
|
636
|
+
branches.push({
|
|
637
|
+
cond,
|
|
638
|
+
then: thenBranch
|
|
639
|
+
});
|
|
640
|
+
}
|
|
641
|
+
this.consume("Else", "Expected \"else\" in if expression");
|
|
642
|
+
const elseBranch = this.parsePipe();
|
|
643
|
+
const endToken = this.consume("End", "Expected \"end\" to close if expression");
|
|
644
|
+
return {
|
|
645
|
+
kind: "If",
|
|
646
|
+
branches,
|
|
647
|
+
else: elseBranch,
|
|
648
|
+
span: spanBetween(start.span, endToken.span)
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
parseArray(start) {
|
|
652
|
+
const items = [];
|
|
653
|
+
if (!this.check("RBracket")) do
|
|
654
|
+
items.push(this.parsePipe());
|
|
655
|
+
while (this.match("Comma"));
|
|
656
|
+
const end = this.consume("RBracket", "Expected \"]\" after array literal");
|
|
657
|
+
return {
|
|
658
|
+
kind: "Array",
|
|
659
|
+
items,
|
|
660
|
+
span: spanBetween(start.span, end.span)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
parseObject(start) {
|
|
664
|
+
const entries = [];
|
|
665
|
+
if (!this.check("RBrace")) do
|
|
666
|
+
entries.push(this.parseObjectEntry());
|
|
667
|
+
while (this.match("Comma"));
|
|
668
|
+
const end = this.consume("RBrace", "Expected \"}\" after object literal");
|
|
669
|
+
return {
|
|
670
|
+
kind: "Object",
|
|
671
|
+
entries,
|
|
672
|
+
span: spanBetween(start.span, end.span)
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
parseObjectEntry() {
|
|
676
|
+
const key = this.parseObjectKey();
|
|
677
|
+
this.consume("Colon", "Expected \":\" after object key");
|
|
678
|
+
return {
|
|
679
|
+
key,
|
|
680
|
+
value: this.parsePipe()
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
parseObjectKey() {
|
|
684
|
+
if (this.match("Identifier")) {
|
|
685
|
+
const token = this.previous();
|
|
686
|
+
return {
|
|
687
|
+
kind: "KeyIdentifier",
|
|
688
|
+
name: String(token.value),
|
|
689
|
+
span: token.span
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
if (this.match("String")) {
|
|
693
|
+
const token = this.previous();
|
|
694
|
+
return {
|
|
695
|
+
kind: "KeyString",
|
|
696
|
+
value: String(token.value),
|
|
697
|
+
span: token.span
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
if (this.match("LParen")) {
|
|
701
|
+
const start = this.previous();
|
|
702
|
+
const expr = this.parseComma();
|
|
703
|
+
const closing = this.consume("RParen", "Expected \")\" after computed key expression");
|
|
704
|
+
return {
|
|
705
|
+
kind: "KeyExpr",
|
|
706
|
+
expr,
|
|
707
|
+
span: spanBetween(start.span, closing.span)
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
throw this.error(this.peek(), "Expected identifier, string, or \"(expr)\" as object key");
|
|
711
|
+
}
|
|
712
|
+
finishFieldAccess(target) {
|
|
713
|
+
if (this.match("Identifier")) {
|
|
714
|
+
const token = this.previous();
|
|
715
|
+
return {
|
|
716
|
+
kind: "FieldAccess",
|
|
717
|
+
target,
|
|
718
|
+
field: String(token.value),
|
|
719
|
+
span: spanBetween(target.span, token.span)
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
if (this.match("String")) {
|
|
723
|
+
const token = this.previous();
|
|
724
|
+
return {
|
|
725
|
+
kind: "FieldAccess",
|
|
726
|
+
target,
|
|
727
|
+
field: String(token.value),
|
|
728
|
+
span: spanBetween(target.span, token.span)
|
|
729
|
+
};
|
|
730
|
+
}
|
|
731
|
+
throw this.error(this.peek(), "Expected field name after \".\"");
|
|
732
|
+
}
|
|
733
|
+
parseLeadingDot(dot) {
|
|
734
|
+
let expr = {
|
|
735
|
+
kind: "Identity",
|
|
736
|
+
span: dot.span
|
|
737
|
+
};
|
|
738
|
+
while (true) {
|
|
739
|
+
if (this.check("Identifier") || this.check("String")) {
|
|
740
|
+
expr = this.finishFieldAccess(expr);
|
|
741
|
+
continue;
|
|
742
|
+
}
|
|
743
|
+
if (this.match("LBracket")) {
|
|
744
|
+
if (this.match("RBracket")) {
|
|
745
|
+
const close = this.previous();
|
|
746
|
+
expr = {
|
|
747
|
+
kind: "Iterate",
|
|
748
|
+
target: expr,
|
|
749
|
+
span: spanBetween(expr.span, close.span)
|
|
750
|
+
};
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
const index = this.parsePipe();
|
|
754
|
+
const closing = this.consume("RBracket", "Expected \"]\" after index expression");
|
|
755
|
+
expr = {
|
|
756
|
+
kind: "IndexAccess",
|
|
757
|
+
target: expr,
|
|
758
|
+
index,
|
|
759
|
+
span: spanBetween(expr.span, closing.span)
|
|
760
|
+
};
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
break;
|
|
764
|
+
}
|
|
765
|
+
return expr;
|
|
766
|
+
}
|
|
767
|
+
finishIdentifier(token) {
|
|
768
|
+
if (this.match("LParen")) {
|
|
769
|
+
const args = this.parseCallArguments();
|
|
770
|
+
const closing = this.consume("RParen", "Expected \")\" after arguments");
|
|
771
|
+
return {
|
|
772
|
+
kind: "Call",
|
|
773
|
+
name: String(token.value),
|
|
774
|
+
args,
|
|
775
|
+
span: spanBetween(token.span, closing.span)
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
return {
|
|
779
|
+
kind: "Call",
|
|
780
|
+
name: String(token.value),
|
|
781
|
+
args: [],
|
|
782
|
+
span: token.span
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
parseCallArguments() {
|
|
786
|
+
const args = [];
|
|
787
|
+
if (this.check("RParen")) return args;
|
|
788
|
+
do
|
|
789
|
+
args.push(this.parsePipe());
|
|
790
|
+
while (this.match("Semicolon"));
|
|
791
|
+
return args;
|
|
792
|
+
}
|
|
793
|
+
literalNode(value, span) {
|
|
794
|
+
return {
|
|
795
|
+
kind: "Literal",
|
|
796
|
+
value,
|
|
797
|
+
span
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
makeBinary(left, right, op) {
|
|
801
|
+
return {
|
|
802
|
+
kind: "Binary",
|
|
803
|
+
op,
|
|
804
|
+
left,
|
|
805
|
+
right,
|
|
806
|
+
span: spanBetween(left.span, right.span)
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
match(kind) {
|
|
810
|
+
if (this.check(kind)) {
|
|
811
|
+
this.advance();
|
|
812
|
+
return true;
|
|
813
|
+
}
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
consume(kind, message) {
|
|
817
|
+
if (this.check(kind)) return this.advance();
|
|
818
|
+
throw this.error(this.peek(), message);
|
|
819
|
+
}
|
|
820
|
+
check(kind) {
|
|
821
|
+
if (this.isAtEnd()) return kind === "EOF";
|
|
822
|
+
return this.peek().kind === kind;
|
|
823
|
+
}
|
|
824
|
+
advance() {
|
|
825
|
+
if (!this.isAtEnd()) this.current += 1;
|
|
826
|
+
return this.previous();
|
|
827
|
+
}
|
|
828
|
+
isAtEnd() {
|
|
829
|
+
return this.peek().kind === "EOF";
|
|
830
|
+
}
|
|
831
|
+
peek() {
|
|
832
|
+
return this.tokens[this.current] ?? {
|
|
833
|
+
kind: "EOF",
|
|
834
|
+
span: {
|
|
835
|
+
start: 0,
|
|
836
|
+
end: 0
|
|
837
|
+
}
|
|
838
|
+
};
|
|
839
|
+
}
|
|
840
|
+
previous() {
|
|
841
|
+
return this.tokens[this.current - 1] ?? {
|
|
842
|
+
kind: "EOF",
|
|
843
|
+
span: {
|
|
844
|
+
start: 0,
|
|
845
|
+
end: 0
|
|
846
|
+
}
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
error(token, message) {
|
|
850
|
+
return new ParseError(message, token.span);
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
const spanBetween = (a, b) => ({
|
|
854
|
+
start: Math.min(a.start, b.start),
|
|
855
|
+
end: Math.max(a.end, b.end)
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
//#endregion
|
|
859
|
+
//#region src/value.ts
|
|
860
|
+
/**
|
|
861
|
+
* Checks if a value is considered "truthy" in jq logic.
|
|
862
|
+
* In jq, only `false` and `null` are falsey; everything else (including 0 and empty strings) is truthy.
|
|
863
|
+
*
|
|
864
|
+
* @param value - The value to check.
|
|
865
|
+
* @returns `true` if truthy, `false` otherwise.
|
|
866
|
+
*/
|
|
867
|
+
const isTruthy = (value) => !(value === false || value === null);
|
|
868
|
+
/**
|
|
869
|
+
* deeply compares two JSON values for equality.
|
|
870
|
+
*
|
|
871
|
+
* @param a - The first value.
|
|
872
|
+
* @param b - The second value.
|
|
873
|
+
* @returns `true` if the values are structurally equal, `false` otherwise.
|
|
874
|
+
*/
|
|
875
|
+
const valueEquals = (a, b) => {
|
|
876
|
+
if (a === b) return true;
|
|
877
|
+
if (typeof a !== typeof b) return false;
|
|
878
|
+
if (typeof a === "number" && typeof b === "number") return Object.is(a, b);
|
|
879
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
880
|
+
if (a.length !== b.length) return false;
|
|
881
|
+
for (let i = 0; i < a.length; i += 1) if (!valueEquals(a[i], b[i])) return false;
|
|
882
|
+
return true;
|
|
883
|
+
}
|
|
884
|
+
if (isPlainObject$1(a) && isPlainObject$1(b)) {
|
|
885
|
+
const aKeys = Object.keys(a).sort();
|
|
886
|
+
const bKeys = Object.keys(b).sort();
|
|
887
|
+
if (aKeys.length !== bKeys.length) return false;
|
|
888
|
+
for (let i = 0; i < aKeys.length; i += 1) {
|
|
889
|
+
const key = aKeys[i];
|
|
890
|
+
if (key !== bKeys[i]) return false;
|
|
891
|
+
if (!valueEquals(a[key], b[key])) return false;
|
|
892
|
+
}
|
|
893
|
+
return true;
|
|
894
|
+
}
|
|
895
|
+
return false;
|
|
896
|
+
};
|
|
897
|
+
/**
|
|
898
|
+
* Compares two JSON values according to standard jq ordering rules.
|
|
899
|
+
*
|
|
900
|
+
* The sort order is: null < false < true < numbers < strings < arrays < objects.
|
|
901
|
+
* Arrays and objects are compared recursively.
|
|
902
|
+
*
|
|
903
|
+
* @param a - The first value.
|
|
904
|
+
* @param b - The second value.
|
|
905
|
+
* @returns `-1` if `a < b`, `0` if `a == b`, and `1` if `a > b`.
|
|
906
|
+
*/
|
|
907
|
+
const compareValues = (a, b) => {
|
|
908
|
+
if (valueEquals(a, b)) return 0;
|
|
909
|
+
const rankDiff = typeRank(a) - typeRank(b);
|
|
910
|
+
if (rankDiff !== 0) return rankDiff < 0 ? -1 : 1;
|
|
911
|
+
if (typeof a === "number" && typeof b === "number") return a < b ? -1 : 1;
|
|
912
|
+
if (typeof a === "string" && typeof b === "string") return a < b ? -1 : 1;
|
|
913
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
914
|
+
const len = Math.min(a.length, b.length);
|
|
915
|
+
for (let i = 0; i < len; i += 1) {
|
|
916
|
+
const cmp = compareValues(a[i], b[i]);
|
|
917
|
+
if (cmp !== 0) return cmp;
|
|
918
|
+
}
|
|
919
|
+
return a.length < b.length ? -1 : 1;
|
|
920
|
+
}
|
|
921
|
+
if (isPlainObject$1(a) && isPlainObject$1(b)) {
|
|
922
|
+
const aKeys = Object.keys(a).sort();
|
|
923
|
+
const bKeys = Object.keys(b).sort();
|
|
924
|
+
const len = Math.min(aKeys.length, bKeys.length);
|
|
925
|
+
for (let i = 0; i < len; i += 1) {
|
|
926
|
+
const key = aKeys[i];
|
|
927
|
+
const keyCmp = compareStrings(key, bKeys[i]);
|
|
928
|
+
if (keyCmp !== 0) return keyCmp;
|
|
929
|
+
const valueCmp = compareValues(a[key], b[key]);
|
|
930
|
+
if (valueCmp !== 0) return valueCmp;
|
|
931
|
+
}
|
|
932
|
+
return aKeys.length < bKeys.length ? -1 : 1;
|
|
933
|
+
}
|
|
934
|
+
return 0;
|
|
935
|
+
};
|
|
936
|
+
const compareStrings = (a, b) => {
|
|
937
|
+
if (a === b) return 0;
|
|
938
|
+
return a < b ? -1 : 1;
|
|
939
|
+
};
|
|
940
|
+
const typeRank = (value) => {
|
|
941
|
+
if (value === null) return 0;
|
|
942
|
+
if (value === false) return 1;
|
|
943
|
+
if (value === true) return 2;
|
|
944
|
+
if (typeof value === "number") return 3;
|
|
945
|
+
if (typeof value === "string") return 4;
|
|
946
|
+
if (Array.isArray(value)) return 5;
|
|
947
|
+
return 6;
|
|
948
|
+
};
|
|
949
|
+
/**
|
|
950
|
+
* Checks if a value is a plain JSON object (and not null or an array).
|
|
951
|
+
*
|
|
952
|
+
* @param value - The value to check.
|
|
953
|
+
* @returns `true` if `value` is a non-null object (and not an array).
|
|
954
|
+
*/
|
|
955
|
+
const isPlainObject$1 = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
956
|
+
/**
|
|
957
|
+
* Returns the type name of a value as a string (e.g., "null", "boolean", "number", "string", "array", "object").
|
|
958
|
+
* This corresponds to the output of the `type` builtin.
|
|
959
|
+
*
|
|
960
|
+
* @param value - The value to inspect.
|
|
961
|
+
* @returns The lower-case type name.
|
|
962
|
+
*/
|
|
963
|
+
const describeType$1 = (value) => {
|
|
964
|
+
if (value === null) return "null";
|
|
965
|
+
if (typeof value === "boolean") return "boolean";
|
|
966
|
+
if (typeof value === "number") return "number";
|
|
967
|
+
if (typeof value === "string") return "string";
|
|
968
|
+
if (Array.isArray(value)) return "array";
|
|
969
|
+
return "object";
|
|
970
|
+
};
|
|
971
|
+
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/builtins.ts
|
|
974
|
+
const emit$1 = (value, span, tracker) => {
|
|
975
|
+
tracker.emit(span);
|
|
976
|
+
return value;
|
|
977
|
+
};
|
|
978
|
+
const ensureIndex = (val) => {
|
|
979
|
+
if (typeof val === "number" && Number.isInteger(val)) return val;
|
|
980
|
+
if (typeof val === "string" && /^-?\d+$/.test(val)) return parseInt(val, 10);
|
|
981
|
+
};
|
|
982
|
+
const stableStringify = (value) => {
|
|
983
|
+
if (value === null) return "null";
|
|
984
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
985
|
+
if (typeof value === "number") return value.toString();
|
|
986
|
+
if (typeof value === "string") return JSON.stringify(value);
|
|
987
|
+
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
988
|
+
return `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
|
|
989
|
+
};
|
|
990
|
+
const builtins = {
|
|
991
|
+
type: {
|
|
992
|
+
name: "type",
|
|
993
|
+
arity: 0,
|
|
994
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
995
|
+
yield emit$1(describeType$1(input), span, tracker);
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
tostring: {
|
|
999
|
+
name: "tostring",
|
|
1000
|
+
arity: 0,
|
|
1001
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1002
|
+
yield emit$1(stableStringify(input), span, tracker);
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
tonumber: {
|
|
1006
|
+
name: "tonumber",
|
|
1007
|
+
arity: 0,
|
|
1008
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1009
|
+
if (typeof input === "number") {
|
|
1010
|
+
yield emit$1(input, span, tracker);
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (typeof input === "string") {
|
|
1014
|
+
const num = Number(input);
|
|
1015
|
+
if (!Number.isFinite(num)) throw new RuntimeError(`Cannot convert string "${input}" to number`, span);
|
|
1016
|
+
yield emit$1(num, span, tracker);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
throw new RuntimeError(`Cannot convert ${describeType$1(input)} to number`, span);
|
|
1020
|
+
}
|
|
1021
|
+
},
|
|
1022
|
+
length: {
|
|
1023
|
+
name: "length",
|
|
1024
|
+
arity: 0,
|
|
1025
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1026
|
+
if (typeof input === "string") yield emit$1(Array.from(input).length, span, tracker);
|
|
1027
|
+
else if (Array.isArray(input)) yield emit$1(input.length, span, tracker);
|
|
1028
|
+
else if (input !== null && typeof input === "object") yield emit$1(Object.keys(input).length, span, tracker);
|
|
1029
|
+
else throw new RuntimeError(`Cannot take length of ${describeType$1(input)}`, span);
|
|
1030
|
+
}
|
|
1031
|
+
},
|
|
1032
|
+
keys: {
|
|
1033
|
+
name: "keys",
|
|
1034
|
+
arity: 0,
|
|
1035
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1036
|
+
if (Array.isArray(input)) yield emit$1(Array.from({ length: input.length }, (_, i) => i), span, tracker);
|
|
1037
|
+
else if (input !== null && typeof input === "object") yield emit$1(Object.keys(input).sort(), span, tracker);
|
|
1038
|
+
else throw new RuntimeError(`keys expects an array or object`, span);
|
|
1039
|
+
}
|
|
1040
|
+
},
|
|
1041
|
+
has: {
|
|
1042
|
+
name: "has",
|
|
1043
|
+
arity: 1,
|
|
1044
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1045
|
+
const keyFilter = args[0];
|
|
1046
|
+
for (const key of evaluate$1(keyFilter, input, env, tracker)) if (Array.isArray(input)) {
|
|
1047
|
+
const idx = ensureIndex(key);
|
|
1048
|
+
yield emit$1(idx !== void 0 && idx >= 0 && idx < input.length, span, tracker);
|
|
1049
|
+
} else if (input !== null && typeof input === "object") {
|
|
1050
|
+
let keyStr;
|
|
1051
|
+
if (typeof key === "string") keyStr = key;
|
|
1052
|
+
else if (typeof key === "number") keyStr = key.toString();
|
|
1053
|
+
else throw new RuntimeError(`has() key must be string or number for object input`, span);
|
|
1054
|
+
yield emit$1(Object.prototype.hasOwnProperty.call(input, keyStr), span, tracker);
|
|
1055
|
+
} else throw new RuntimeError(`has() expects an array or object input`, span);
|
|
1056
|
+
}
|
|
1057
|
+
},
|
|
1058
|
+
error: {
|
|
1059
|
+
name: "error",
|
|
1060
|
+
arity: 1,
|
|
1061
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1062
|
+
for (const msg of evaluate$1(args[0], input, env, tracker)) throw new RuntimeError(typeof msg === "string" ? msg : stableStringify(msg), span);
|
|
1063
|
+
}
|
|
1064
|
+
},
|
|
1065
|
+
map: {
|
|
1066
|
+
name: "map",
|
|
1067
|
+
arity: 1,
|
|
1068
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1069
|
+
if (!Array.isArray(input)) throw new RuntimeError("map expects an array", span);
|
|
1070
|
+
const result = [];
|
|
1071
|
+
const filter = args[0];
|
|
1072
|
+
for (const item of input) {
|
|
1073
|
+
tracker.step(span);
|
|
1074
|
+
for (const output of evaluate$1(filter, item, env, tracker)) result.push(output);
|
|
1075
|
+
}
|
|
1076
|
+
yield emit$1(result, span, tracker);
|
|
1077
|
+
}
|
|
1078
|
+
},
|
|
1079
|
+
select: {
|
|
1080
|
+
name: "select",
|
|
1081
|
+
arity: 1,
|
|
1082
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1083
|
+
const filter = args[0];
|
|
1084
|
+
for (const res of evaluate$1(filter, input, env, tracker)) if (isTruthy(res)) {
|
|
1085
|
+
yield emit$1(input, span, tracker);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
},
|
|
1090
|
+
sort: {
|
|
1091
|
+
name: "sort",
|
|
1092
|
+
arity: 0,
|
|
1093
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1094
|
+
if (!Array.isArray(input)) throw new RuntimeError("sort expects an array", span);
|
|
1095
|
+
const sorted = sortStable(input, (a, b) => compareValues(a, b));
|
|
1096
|
+
tracker.step(span);
|
|
1097
|
+
yield emit$1(sorted, span, tracker);
|
|
1098
|
+
}
|
|
1099
|
+
},
|
|
1100
|
+
sort_by: {
|
|
1101
|
+
name: "sort_by",
|
|
1102
|
+
arity: 1,
|
|
1103
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1104
|
+
if (!Array.isArray(input)) throw new RuntimeError("sort_by expects an array", span);
|
|
1105
|
+
const filter = args[0];
|
|
1106
|
+
const pairs = [];
|
|
1107
|
+
for (const item of input) {
|
|
1108
|
+
tracker.step(span);
|
|
1109
|
+
const keys = Array.from(evaluate$1(filter, item, env, tracker));
|
|
1110
|
+
if (keys.length !== 1) throw new RuntimeError("sort_by key expression must return exactly one value", span);
|
|
1111
|
+
pairs.push({
|
|
1112
|
+
val: item,
|
|
1113
|
+
key: keys[0]
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
yield emit$1(sortStable(pairs, (a, b) => compareValues(a.key, b.key)).map((p) => p.val), span, tracker);
|
|
1117
|
+
}
|
|
1118
|
+
},
|
|
1119
|
+
unique: {
|
|
1120
|
+
name: "unique",
|
|
1121
|
+
arity: 0,
|
|
1122
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1123
|
+
if (!Array.isArray(input)) throw new RuntimeError("unique expects an array", span);
|
|
1124
|
+
const seen = [];
|
|
1125
|
+
const result = [];
|
|
1126
|
+
for (const item of input) {
|
|
1127
|
+
tracker.step(span);
|
|
1128
|
+
if (!seen.some((s) => valueEquals(s, item))) {
|
|
1129
|
+
seen.push(item);
|
|
1130
|
+
result.push(item);
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
yield emit$1(result, span, tracker);
|
|
1134
|
+
}
|
|
1135
|
+
},
|
|
1136
|
+
unique_by: {
|
|
1137
|
+
name: "unique_by",
|
|
1138
|
+
arity: 1,
|
|
1139
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1140
|
+
if (!Array.isArray(input)) throw new RuntimeError("unique_by expects an array", span);
|
|
1141
|
+
const filter = args[0];
|
|
1142
|
+
const seenKeys = [];
|
|
1143
|
+
const result = [];
|
|
1144
|
+
for (const item of input) {
|
|
1145
|
+
tracker.step(span);
|
|
1146
|
+
const keys = Array.from(evaluate$1(filter, item, env, tracker));
|
|
1147
|
+
if (keys.length !== 1) throw new RuntimeError("unique_by key expression must return exactly one value", span);
|
|
1148
|
+
const key = keys[0];
|
|
1149
|
+
if (!seenKeys.some((s) => valueEquals(s, key))) {
|
|
1150
|
+
seenKeys.push(key);
|
|
1151
|
+
result.push(item);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
yield emit$1(result, span, tracker);
|
|
1155
|
+
}
|
|
1156
|
+
},
|
|
1157
|
+
to_entries: {
|
|
1158
|
+
name: "to_entries",
|
|
1159
|
+
arity: 0,
|
|
1160
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1161
|
+
if (Array.isArray(input)) yield emit$1(input.map((v, i) => ({
|
|
1162
|
+
key: i,
|
|
1163
|
+
value: v
|
|
1164
|
+
})), span, tracker);
|
|
1165
|
+
else if (input !== null && typeof input === "object") yield emit$1(Object.keys(input).sort().map((k) => ({
|
|
1166
|
+
key: k,
|
|
1167
|
+
value: input[k]
|
|
1168
|
+
})), span, tracker);
|
|
1169
|
+
else throw new RuntimeError("to_entries expects array or object", span);
|
|
1170
|
+
}
|
|
1171
|
+
},
|
|
1172
|
+
from_entries: {
|
|
1173
|
+
name: "from_entries",
|
|
1174
|
+
arity: 0,
|
|
1175
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1176
|
+
if (!Array.isArray(input)) throw new RuntimeError("from_entries expects an array", span);
|
|
1177
|
+
const result = {};
|
|
1178
|
+
for (const item of input) {
|
|
1179
|
+
tracker.step(span);
|
|
1180
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) throw new RuntimeError("from_entries expects array of objects", span);
|
|
1181
|
+
const obj = item;
|
|
1182
|
+
if (!("key" in obj) || !("value" in obj)) throw new RuntimeError("from_entries items must have \"key\" and \"value\"", span);
|
|
1183
|
+
const key = obj["key"];
|
|
1184
|
+
if (typeof key !== "string") throw new RuntimeError("from_entries object keys must be strings", span);
|
|
1185
|
+
result[key] = obj["value"];
|
|
1186
|
+
}
|
|
1187
|
+
yield emit$1(result, span, tracker);
|
|
1188
|
+
}
|
|
1189
|
+
},
|
|
1190
|
+
with_entries: {
|
|
1191
|
+
name: "with_entries",
|
|
1192
|
+
arity: 1,
|
|
1193
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1194
|
+
let entries;
|
|
1195
|
+
if (Array.isArray(input)) entries = input.map((v, i) => ({
|
|
1196
|
+
key: i,
|
|
1197
|
+
value: v
|
|
1198
|
+
}));
|
|
1199
|
+
else if (input !== null && typeof input === "object") entries = Object.keys(input).sort().map((k) => ({
|
|
1200
|
+
key: k,
|
|
1201
|
+
value: input[k]
|
|
1202
|
+
}));
|
|
1203
|
+
else throw new RuntimeError("with_entries expects array or object", span);
|
|
1204
|
+
const transformed = [];
|
|
1205
|
+
const filter = args[0];
|
|
1206
|
+
for (const entry of entries) {
|
|
1207
|
+
tracker.step(span);
|
|
1208
|
+
for (const outVar of evaluate$1(filter, entry, env, tracker)) transformed.push(outVar);
|
|
1209
|
+
}
|
|
1210
|
+
const result = {};
|
|
1211
|
+
for (const item of transformed) {
|
|
1212
|
+
if (item === null || typeof item !== "object" || Array.isArray(item)) throw new RuntimeError("with_entries filter must produce objects", span);
|
|
1213
|
+
const obj = item;
|
|
1214
|
+
if (!("key" in obj) || !("value" in obj)) throw new RuntimeError("with_entries items must have \"key\" and \"value\"", span);
|
|
1215
|
+
const key = obj["key"];
|
|
1216
|
+
if (typeof key !== "string") throw new RuntimeError("with_entries keys must be strings", span);
|
|
1217
|
+
result[key] = obj["value"];
|
|
1218
|
+
}
|
|
1219
|
+
yield emit$1(result, span, tracker);
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
split: {
|
|
1223
|
+
name: "split",
|
|
1224
|
+
arity: 1,
|
|
1225
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1226
|
+
if (typeof input !== "string") throw new RuntimeError("split input must be a string", span);
|
|
1227
|
+
const sepGen = evaluate$1(args[0], input, env, tracker);
|
|
1228
|
+
for (const sep of sepGen) {
|
|
1229
|
+
if (typeof sep !== "string") throw new RuntimeError("split separator must be a string", span);
|
|
1230
|
+
yield emit$1(input.split(sep), span, tracker);
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
},
|
|
1234
|
+
join: {
|
|
1235
|
+
name: "join",
|
|
1236
|
+
arity: 1,
|
|
1237
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1238
|
+
if (!Array.isArray(input)) throw new RuntimeError("join input must be an array", span);
|
|
1239
|
+
const sepGen = evaluate$1(args[0], input, env, tracker);
|
|
1240
|
+
for (const sep of sepGen) {
|
|
1241
|
+
if (typeof sep !== "string") throw new RuntimeError("join separator must be a string", span);
|
|
1242
|
+
const parts = [];
|
|
1243
|
+
for (const item of input) {
|
|
1244
|
+
if (typeof item !== "string") throw new RuntimeError(`join expects strings, but got ${describeType$1(item)}`, span);
|
|
1245
|
+
parts.push(item);
|
|
1246
|
+
}
|
|
1247
|
+
yield emit$1(parts.join(sep), span, tracker);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
},
|
|
1251
|
+
startswith: {
|
|
1252
|
+
name: "startswith",
|
|
1253
|
+
arity: 1,
|
|
1254
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1255
|
+
if (typeof input !== "string") throw new RuntimeError("startswith input must be a string", span);
|
|
1256
|
+
const prefixGen = evaluate$1(args[0], input, env, tracker);
|
|
1257
|
+
for (const prefix of prefixGen) {
|
|
1258
|
+
if (typeof prefix !== "string") throw new RuntimeError("startswith prefix must be a string", span);
|
|
1259
|
+
yield emit$1(input.startsWith(prefix), span, tracker);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
},
|
|
1263
|
+
endswith: {
|
|
1264
|
+
name: "endswith",
|
|
1265
|
+
arity: 1,
|
|
1266
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1267
|
+
if (typeof input !== "string") throw new RuntimeError("endswith input must be a string", span);
|
|
1268
|
+
const suffixGen = evaluate$1(args[0], input, env, tracker);
|
|
1269
|
+
for (const suffix of suffixGen) {
|
|
1270
|
+
if (typeof suffix !== "string") throw new RuntimeError("endswith suffix must be a string", span);
|
|
1271
|
+
yield emit$1(input.endsWith(suffix), span, tracker);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
},
|
|
1275
|
+
contains: {
|
|
1276
|
+
name: "contains",
|
|
1277
|
+
arity: 1,
|
|
1278
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1279
|
+
const bGen = evaluate$1(args[0], input, env, tracker);
|
|
1280
|
+
for (const b of bGen) yield emit$1(checkContains(input, b), span, tracker);
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
paths: {
|
|
1284
|
+
name: "paths",
|
|
1285
|
+
arity: 0,
|
|
1286
|
+
apply: function* (input, _args, _env, tracker, _eval, span) {
|
|
1287
|
+
yield* traversePaths(input, [], span, tracker);
|
|
1288
|
+
}
|
|
1289
|
+
},
|
|
1290
|
+
getpath: {
|
|
1291
|
+
name: "getpath",
|
|
1292
|
+
arity: 1,
|
|
1293
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1294
|
+
for (const pathVal of evaluate$1(args[0], input, env, tracker)) yield emit$1(getPath(input, ensurePath(pathVal, span)) ?? null, span, tracker);
|
|
1295
|
+
}
|
|
1296
|
+
},
|
|
1297
|
+
setpath: {
|
|
1298
|
+
name: "setpath",
|
|
1299
|
+
arity: 2,
|
|
1300
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1301
|
+
const paths = Array.from(evaluate$1(args[0], input, env, tracker));
|
|
1302
|
+
const values = Array.from(evaluate$1(args[1], input, env, tracker));
|
|
1303
|
+
for (const pathVal of paths) {
|
|
1304
|
+
const path = ensurePath(pathVal, span);
|
|
1305
|
+
for (const val of values) yield emit$1(updatePath(input, path, () => val, span) ?? null, span, tracker);
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
},
|
|
1309
|
+
delpaths: {
|
|
1310
|
+
name: "delpaths",
|
|
1311
|
+
arity: 1,
|
|
1312
|
+
apply: function* (input, args, env, tracker, evaluate$1, span) {
|
|
1313
|
+
for (const pathsVal of evaluate$1(args[0], input, env, tracker)) {
|
|
1314
|
+
if (!Array.isArray(pathsVal)) throw new RuntimeError("delpaths expects an array of paths", span);
|
|
1315
|
+
yield emit$1(deletePaths(input, pathsVal.map((p) => ensurePath(p, span)), span), span, tracker);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
const checkContains = (a, b) => {
|
|
1321
|
+
if (a === b) return true;
|
|
1322
|
+
if (typeof a !== typeof b) return false;
|
|
1323
|
+
if (typeof a === "string" && typeof b === "string") return a.includes(b);
|
|
1324
|
+
if (Array.isArray(a) && Array.isArray(b)) return b.every((bItem) => a.some((aItem) => checkContains(aItem, bItem)));
|
|
1325
|
+
if (isPlainObject$1(a) && isPlainObject$1(b)) {
|
|
1326
|
+
const keys = Object.keys(b);
|
|
1327
|
+
for (const key of keys) {
|
|
1328
|
+
if (!Object.prototype.hasOwnProperty.call(a, key)) return false;
|
|
1329
|
+
const valA = a[key];
|
|
1330
|
+
const valB = b[key];
|
|
1331
|
+
if (!checkContains(valA, valB)) return false;
|
|
1332
|
+
}
|
|
1333
|
+
return true;
|
|
1334
|
+
}
|
|
1335
|
+
return valueEquals(a, b);
|
|
1336
|
+
};
|
|
1337
|
+
/**
|
|
1338
|
+
* Recursively traverses a JSON value to find all paths to leaf nodes.
|
|
1339
|
+
* Yields paths as arrays of strings/numbers.
|
|
1340
|
+
*/
|
|
1341
|
+
function* traversePaths(root, currentPath, span, tracker) {
|
|
1342
|
+
tracker.step(span);
|
|
1343
|
+
if (root === null || typeof root !== "object" || Array.isArray(root) && root.length === 0 || isPlainObject$1(root) && Object.keys(root).length === 0) {
|
|
1344
|
+
yield emit$1([...currentPath], span, tracker);
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
if (Array.isArray(root)) for (let i = 0; i < root.length; i++) yield* traversePaths(root[i], [...currentPath, i], span, tracker);
|
|
1348
|
+
else if (isPlainObject$1(root)) {
|
|
1349
|
+
const keys = Object.keys(root).sort();
|
|
1350
|
+
for (const key of keys) yield* traversePaths(root[key], [...currentPath, key], span, tracker);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
const isPath = (val) => {
|
|
1354
|
+
if (!Array.isArray(val)) return false;
|
|
1355
|
+
return val.every((p) => typeof p === "string" || typeof p === "number" && Number.isInteger(p));
|
|
1356
|
+
};
|
|
1357
|
+
const ensurePath = (val, span) => {
|
|
1358
|
+
if (isPath(val)) return val;
|
|
1359
|
+
throw new RuntimeError("Path must be an array of strings or integers", span);
|
|
1360
|
+
};
|
|
1361
|
+
const getPath = (root, path) => {
|
|
1362
|
+
let curr = root;
|
|
1363
|
+
for (const part of path) {
|
|
1364
|
+
if (curr === null) return void 0;
|
|
1365
|
+
if (typeof part === "string" && isPlainObject$1(curr)) {
|
|
1366
|
+
if (!Object.prototype.hasOwnProperty.call(curr, part)) return void 0;
|
|
1367
|
+
curr = curr[part];
|
|
1368
|
+
} else if (typeof part === "number" && Array.isArray(curr)) {
|
|
1369
|
+
if (part < 0 || part >= curr.length) return void 0;
|
|
1370
|
+
curr = curr[part];
|
|
1371
|
+
} else return;
|
|
1372
|
+
}
|
|
1373
|
+
return curr;
|
|
1374
|
+
};
|
|
1375
|
+
/**
|
|
1376
|
+
* Immutably updates a value at a given path.
|
|
1377
|
+
* Creates intermediate objects/arrays as needed.
|
|
1378
|
+
*/
|
|
1379
|
+
const updatePath = (root, path, updateFn, span, depth = 0) => {
|
|
1380
|
+
if (path.length === 0) return updateFn(root);
|
|
1381
|
+
const [head, ...tail] = path;
|
|
1382
|
+
if (typeof head === "string") {
|
|
1383
|
+
let obj = {};
|
|
1384
|
+
if (isPlainObject$1(root)) obj = { ...root };
|
|
1385
|
+
else if (root === null) obj = {};
|
|
1386
|
+
else throw new RuntimeError(`Cannot index ${describeType$1(root)} with string "${head}"`, span);
|
|
1387
|
+
const newVal = updatePath((Object.prototype.hasOwnProperty.call(obj, head) ? obj[head] : void 0) ?? null, tail, updateFn, span, depth + 1);
|
|
1388
|
+
if (newVal === void 0) delete obj[head];
|
|
1389
|
+
else obj[head] = newVal;
|
|
1390
|
+
return obj;
|
|
1391
|
+
}
|
|
1392
|
+
if (typeof head === "number") {
|
|
1393
|
+
let arr = [];
|
|
1394
|
+
if (Array.isArray(root)) arr = [...root];
|
|
1395
|
+
else if (root === null) arr = [];
|
|
1396
|
+
else throw new RuntimeError(`Cannot index ${describeType$1(root)} with number ${head}`, span);
|
|
1397
|
+
const idx = head < 0 ? arr.length + head : head;
|
|
1398
|
+
if (idx < 0) throw new RuntimeError("Invalid negative index", span);
|
|
1399
|
+
const newVal = updatePath((idx < arr.length ? arr[idx] : null) ?? null, tail, updateFn, span, depth + 1);
|
|
1400
|
+
if (newVal === void 0) if (idx >= arr.length) {
|
|
1401
|
+
while (arr.length < idx) arr.push(null);
|
|
1402
|
+
arr.push(newVal);
|
|
1403
|
+
} else arr[idx] = newVal;
|
|
1404
|
+
else if (idx >= arr.length) {
|
|
1405
|
+
while (arr.length < idx) arr.push(null);
|
|
1406
|
+
arr.push(newVal);
|
|
1407
|
+
} else arr[idx] = newVal;
|
|
1408
|
+
return arr;
|
|
1409
|
+
}
|
|
1410
|
+
throw new RuntimeError(`Path segment must be string or integer`, span);
|
|
1411
|
+
};
|
|
1412
|
+
const deletePaths = (root, paths, span) => {
|
|
1413
|
+
if (paths.some((p) => p.length === 0)) return null;
|
|
1414
|
+
if (isPlainObject$1(root)) {
|
|
1415
|
+
const result = { ...root };
|
|
1416
|
+
const relevantPaths = paths.filter((p) => p.length > 0 && typeof p[0] === "string");
|
|
1417
|
+
const byKey = {};
|
|
1418
|
+
for (const p of relevantPaths) {
|
|
1419
|
+
const key = p[0];
|
|
1420
|
+
if (!byKey[key]) byKey[key] = [];
|
|
1421
|
+
byKey[key].push(p.slice(1));
|
|
1422
|
+
}
|
|
1423
|
+
for (const key of Object.keys(byKey)) {
|
|
1424
|
+
const tails = byKey[key];
|
|
1425
|
+
if (tails.some((t) => t.length === 0)) delete result[key];
|
|
1426
|
+
else result[key] = deletePaths(root[key], tails, span);
|
|
1427
|
+
}
|
|
1428
|
+
return result;
|
|
1429
|
+
}
|
|
1430
|
+
if (Array.isArray(root)) {
|
|
1431
|
+
const relevantPaths = paths.filter((p) => p.length > 0 && typeof p[0] === "number");
|
|
1432
|
+
const actions = {};
|
|
1433
|
+
for (const p of relevantPaths) {
|
|
1434
|
+
let idx = p[0];
|
|
1435
|
+
if (idx < 0) idx = root.length + idx;
|
|
1436
|
+
if (idx < 0 || idx >= root.length) continue;
|
|
1437
|
+
if (!actions[idx]) actions[idx] = [];
|
|
1438
|
+
actions[idx].push(p.slice(1));
|
|
1439
|
+
}
|
|
1440
|
+
const finalArr = [];
|
|
1441
|
+
for (let i = 0; i < root.length; i++) {
|
|
1442
|
+
const tails = actions[i];
|
|
1443
|
+
if (tails) if (tails.some((t) => t.length === 0)) continue;
|
|
1444
|
+
else finalArr.push(deletePaths(root[i], tails, span));
|
|
1445
|
+
else finalArr.push(root[i]);
|
|
1446
|
+
}
|
|
1447
|
+
return finalArr;
|
|
1448
|
+
}
|
|
1449
|
+
return root;
|
|
1450
|
+
};
|
|
1451
|
+
function sortStable(arr, compare) {
|
|
1452
|
+
return arr.map((item, index) => ({
|
|
1453
|
+
item,
|
|
1454
|
+
index
|
|
1455
|
+
})).sort((a, b) => {
|
|
1456
|
+
const cmp = compare(a.item, b.item);
|
|
1457
|
+
return cmp !== 0 ? cmp : a.index - b.index;
|
|
1458
|
+
}).map((p) => p.item);
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
//#endregion
|
|
1462
|
+
//#region src/validate.ts
|
|
1463
|
+
/**
|
|
1464
|
+
* Validates the AST for correctness and supported features.
|
|
1465
|
+
* Checks for unknown function calls and arity mismatches.
|
|
1466
|
+
*
|
|
1467
|
+
* @param node - The root AST node to validate.
|
|
1468
|
+
* @throws {ValidationError} If validation fails.
|
|
1469
|
+
*/
|
|
1470
|
+
const validate = (node) => {
|
|
1471
|
+
visit(node);
|
|
1472
|
+
};
|
|
1473
|
+
const visit = (node) => {
|
|
1474
|
+
switch (node.kind) {
|
|
1475
|
+
case "Identity":
|
|
1476
|
+
case "Literal":
|
|
1477
|
+
case "Var": return;
|
|
1478
|
+
case "FieldAccess":
|
|
1479
|
+
visit(node.target);
|
|
1480
|
+
return;
|
|
1481
|
+
case "IndexAccess":
|
|
1482
|
+
visit(node.target);
|
|
1483
|
+
visit(node.index);
|
|
1484
|
+
return;
|
|
1485
|
+
case "Array":
|
|
1486
|
+
node.items.forEach(visit);
|
|
1487
|
+
return;
|
|
1488
|
+
case "Object":
|
|
1489
|
+
node.entries.forEach((entry) => {
|
|
1490
|
+
if (entry.key.kind === "KeyExpr") visit(entry.key.expr);
|
|
1491
|
+
visit(entry.value);
|
|
1492
|
+
});
|
|
1493
|
+
return;
|
|
1494
|
+
case "Pipe":
|
|
1495
|
+
case "Comma":
|
|
1496
|
+
case "Alt":
|
|
1497
|
+
visit(node.left);
|
|
1498
|
+
visit(node.right);
|
|
1499
|
+
return;
|
|
1500
|
+
case "Binary":
|
|
1501
|
+
case "Bool":
|
|
1502
|
+
visit(node.left);
|
|
1503
|
+
visit(node.right);
|
|
1504
|
+
return;
|
|
1505
|
+
case "Unary":
|
|
1506
|
+
visit(node.expr);
|
|
1507
|
+
return;
|
|
1508
|
+
case "If":
|
|
1509
|
+
node.branches.forEach((branch) => {
|
|
1510
|
+
visit(branch.cond);
|
|
1511
|
+
visit(branch.then);
|
|
1512
|
+
});
|
|
1513
|
+
visit(node.else);
|
|
1514
|
+
return;
|
|
1515
|
+
case "As":
|
|
1516
|
+
visit(node.bind);
|
|
1517
|
+
visit(node.body);
|
|
1518
|
+
return;
|
|
1519
|
+
case "Call": {
|
|
1520
|
+
const builtin = builtins[node.name];
|
|
1521
|
+
if (!builtin) throw new ValidationError(`Unknown function: ${node.name}`, node.span);
|
|
1522
|
+
if (builtin.arity !== node.args.length) throw new ValidationError(`Function ${node.name} expects ${builtin.arity} arguments, but got ${node.args.length}`, node.span);
|
|
1523
|
+
node.args.forEach(visit);
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
case "Reduce":
|
|
1527
|
+
visit(node.source);
|
|
1528
|
+
visit(node.init);
|
|
1529
|
+
visit(node.update);
|
|
1530
|
+
return;
|
|
1531
|
+
case "Foreach":
|
|
1532
|
+
visit(node.source);
|
|
1533
|
+
visit(node.init);
|
|
1534
|
+
visit(node.update);
|
|
1535
|
+
if (node.extract) visit(node.extract);
|
|
1536
|
+
return;
|
|
1537
|
+
case "Try":
|
|
1538
|
+
visit(node.body);
|
|
1539
|
+
if (node.handler) visit(node.handler);
|
|
1540
|
+
return;
|
|
1541
|
+
case "Recurse":
|
|
1542
|
+
case "Iterate": return;
|
|
1543
|
+
default: return node;
|
|
1544
|
+
}
|
|
1545
|
+
};
|
|
1546
|
+
|
|
1547
|
+
//#endregion
|
|
1548
|
+
//#region src/limits.ts
|
|
1549
|
+
const DEFAULT_LIMITS = {
|
|
1550
|
+
maxSteps: 1e5,
|
|
1551
|
+
maxDepth: 200,
|
|
1552
|
+
maxOutputs: 1e4
|
|
1553
|
+
};
|
|
1554
|
+
/**
|
|
1555
|
+
* Resolves a partial limits configuration into a complete one with defaults.
|
|
1556
|
+
*
|
|
1557
|
+
* @param config - The user-provided configuration.
|
|
1558
|
+
* @returns The resolved limits.
|
|
1559
|
+
*/
|
|
1560
|
+
const resolveLimits = (config = {}) => ({
|
|
1561
|
+
maxSteps: config.maxSteps ?? DEFAULT_LIMITS.maxSteps,
|
|
1562
|
+
maxDepth: config.maxDepth ?? DEFAULT_LIMITS.maxDepth,
|
|
1563
|
+
maxOutputs: config.maxOutputs ?? DEFAULT_LIMITS.maxOutputs
|
|
1564
|
+
});
|
|
1565
|
+
/**
|
|
1566
|
+
* Tracks execution usage against defined limits.
|
|
1567
|
+
* Throws {@link RuntimeError} if any limit is exceeded.
|
|
1568
|
+
*/
|
|
1569
|
+
var LimitTracker = class {
|
|
1570
|
+
steps = 0;
|
|
1571
|
+
depth = 0;
|
|
1572
|
+
outputs = 0;
|
|
1573
|
+
constructor(limits) {
|
|
1574
|
+
this.limits = limits;
|
|
1575
|
+
}
|
|
1576
|
+
/**
|
|
1577
|
+
* Records a single execution step (AST node visit).
|
|
1578
|
+
* @param span - The source span for error reporting.
|
|
1579
|
+
*/
|
|
1580
|
+
step(span) {
|
|
1581
|
+
this.steps += 1;
|
|
1582
|
+
if (this.steps > this.limits.maxSteps) throw new RuntimeError("Step limit exceeded", span);
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Enters a new scope/stack frame, incrementing the depth counter.
|
|
1586
|
+
* @param span - The source span for error reporting.
|
|
1587
|
+
*/
|
|
1588
|
+
enter(span) {
|
|
1589
|
+
this.depth += 1;
|
|
1590
|
+
if (this.depth > this.limits.maxDepth) throw new RuntimeError("Max depth exceeded", span);
|
|
1591
|
+
}
|
|
1592
|
+
/**
|
|
1593
|
+
* Exits the current scope, decrementing the depth counter.
|
|
1594
|
+
*/
|
|
1595
|
+
exit() {
|
|
1596
|
+
this.depth = Math.max(0, this.depth - 1);
|
|
1597
|
+
}
|
|
1598
|
+
/**
|
|
1599
|
+
* Records an output value being emitted.
|
|
1600
|
+
* @param span - The source span for error reporting.
|
|
1601
|
+
*/
|
|
1602
|
+
emit(span) {
|
|
1603
|
+
this.outputs += 1;
|
|
1604
|
+
if (this.outputs > this.limits.maxOutputs) throw new RuntimeError("Output limit exceeded", span);
|
|
1605
|
+
}
|
|
1606
|
+
};
|
|
1607
|
+
|
|
1608
|
+
//#endregion
|
|
1609
|
+
//#region src/eval.ts
|
|
1610
|
+
const runAst = (ast, input, options = {}) => {
|
|
1611
|
+
const tracker = new LimitTracker(resolveLimits(options.limits));
|
|
1612
|
+
const env = [/* @__PURE__ */ new Map()];
|
|
1613
|
+
return Array.from(evaluate(ast, input, env, tracker));
|
|
1614
|
+
};
|
|
1615
|
+
/**
|
|
1616
|
+
* Evaluates an AST node against an input value.
|
|
1617
|
+
* This is a generator function that yields results lazily.
|
|
1618
|
+
*
|
|
1619
|
+
* @param node - The AST node to evaluate.
|
|
1620
|
+
* @param input - The current input value (context).
|
|
1621
|
+
* @param env - The variable environment stack.
|
|
1622
|
+
* @param tracker - Limits tracker for cycle/output limits.
|
|
1623
|
+
*/
|
|
1624
|
+
function* evaluate(node, input, env, tracker) {
|
|
1625
|
+
tracker.step(node.span);
|
|
1626
|
+
tracker.enter(node.span);
|
|
1627
|
+
try {
|
|
1628
|
+
switch (node.kind) {
|
|
1629
|
+
case "Identity":
|
|
1630
|
+
yield emit(input, node.span, tracker);
|
|
1631
|
+
return;
|
|
1632
|
+
case "Literal":
|
|
1633
|
+
yield emit(node.value, node.span, tracker);
|
|
1634
|
+
return;
|
|
1635
|
+
case "Var":
|
|
1636
|
+
yield emit(getVar(env, node.name, node.span), node.span, tracker);
|
|
1637
|
+
return;
|
|
1638
|
+
case "FieldAccess":
|
|
1639
|
+
yield* evalField(node, input, env, tracker);
|
|
1640
|
+
return;
|
|
1641
|
+
case "IndexAccess":
|
|
1642
|
+
yield* evalIndex(node, input, env, tracker);
|
|
1643
|
+
return;
|
|
1644
|
+
case "Array":
|
|
1645
|
+
yield emit(buildArray(node, input, env, tracker), node.span, tracker);
|
|
1646
|
+
return;
|
|
1647
|
+
case "Object":
|
|
1648
|
+
yield* buildObjects(node, input, env, tracker);
|
|
1649
|
+
return;
|
|
1650
|
+
case "Pipe":
|
|
1651
|
+
for (const left of evaluate(node.left, input, env, tracker)) yield* evaluate(node.right, left, env, tracker);
|
|
1652
|
+
return;
|
|
1653
|
+
case "Comma":
|
|
1654
|
+
yield* evaluate(node.left, input, env, tracker);
|
|
1655
|
+
yield* evaluate(node.right, input, env, tracker);
|
|
1656
|
+
return;
|
|
1657
|
+
case "Alt": {
|
|
1658
|
+
const valid = Array.from(evaluate(node.left, input, env, tracker)).filter((v) => v !== null && v !== false);
|
|
1659
|
+
if (valid.length > 0) for (const v of valid) yield v;
|
|
1660
|
+
else yield* evaluate(node.right, input, env, tracker);
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
case "Unary":
|
|
1664
|
+
if (node.op === "Not") for (const value of evaluate(node.expr, input, env, tracker)) yield emit(!isTruthy(value), node.span, tracker);
|
|
1665
|
+
else for (const value of evaluate(node.expr, input, env, tracker)) yield emit(applyUnaryNeg(value, node.span), node.span, tracker);
|
|
1666
|
+
return;
|
|
1667
|
+
case "Binary":
|
|
1668
|
+
yield* evalBinary(node, input, env, tracker);
|
|
1669
|
+
return;
|
|
1670
|
+
case "Bool":
|
|
1671
|
+
if (node.op === "Or") for (const left of evaluate(node.left, input, env, tracker)) if (isTruthy(left)) yield emit(true, node.span, tracker);
|
|
1672
|
+
else for (const right of evaluate(node.right, input, env, tracker)) yield emit(isTruthy(right), node.span, tracker);
|
|
1673
|
+
else for (const left of evaluate(node.left, input, env, tracker)) if (!isTruthy(left)) yield emit(false, node.span, tracker);
|
|
1674
|
+
else for (const right of evaluate(node.right, input, env, tracker)) yield emit(isTruthy(right), node.span, tracker);
|
|
1675
|
+
return;
|
|
1676
|
+
case "If":
|
|
1677
|
+
for (const branch of node.branches) {
|
|
1678
|
+
const condValues = Array.from(evaluate(branch.cond, input, env, tracker));
|
|
1679
|
+
if (condValues.length > 1) throw new RuntimeError("Condition produced multiple values", branch.cond.span);
|
|
1680
|
+
if (condValues.length === 1 && isTruthy(condValues[0])) {
|
|
1681
|
+
yield* evaluate(branch.then, input, env, tracker);
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
yield* evaluate(node.else, input, env, tracker);
|
|
1686
|
+
return;
|
|
1687
|
+
case "As": {
|
|
1688
|
+
const boundValues = Array.from(evaluate(node.bind, input, env, tracker));
|
|
1689
|
+
for (const value of boundValues) {
|
|
1690
|
+
pushBinding(env, node.name, value);
|
|
1691
|
+
try {
|
|
1692
|
+
yield* evaluate(node.body, input, env, tracker);
|
|
1693
|
+
} finally {
|
|
1694
|
+
popBinding(env);
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
case "Call": {
|
|
1700
|
+
const builtin = builtins[node.name];
|
|
1701
|
+
if (!builtin) throw new RuntimeError(`Unknown function: ${node.name}/${node.args.length}`, node.span);
|
|
1702
|
+
yield* builtin.apply(input, node.args, env, tracker, evaluate, node.span);
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
case "Reduce":
|
|
1706
|
+
yield* evalReduce(node, input, env, tracker);
|
|
1707
|
+
return;
|
|
1708
|
+
case "Foreach":
|
|
1709
|
+
yield* evalForeach(node, input, env, tracker);
|
|
1710
|
+
return;
|
|
1711
|
+
case "Try":
|
|
1712
|
+
yield* evalTry(node, input, env, tracker);
|
|
1713
|
+
return;
|
|
1714
|
+
case "Recurse":
|
|
1715
|
+
yield* evalRecurse(node, input, env, tracker);
|
|
1716
|
+
return;
|
|
1717
|
+
case "Iterate":
|
|
1718
|
+
yield* evalIterate(node, input, env, tracker);
|
|
1719
|
+
return;
|
|
1720
|
+
}
|
|
1721
|
+
} finally {
|
|
1722
|
+
tracker.exit();
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
const emit = (value, span, tracker) => {
|
|
1726
|
+
tracker.emit(span);
|
|
1727
|
+
return value;
|
|
1728
|
+
};
|
|
1729
|
+
const evalField = function* (node, input, env, tracker) {
|
|
1730
|
+
for (const container of evaluate(node.target, input, env, tracker)) {
|
|
1731
|
+
if (container === null) {
|
|
1732
|
+
yield emit(null, node.span, tracker);
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
if (isPlainObject(container)) {
|
|
1736
|
+
yield emit(Object.prototype.hasOwnProperty.call(container, node.field) ? container[node.field] : null, node.span, tracker);
|
|
1737
|
+
continue;
|
|
1738
|
+
}
|
|
1739
|
+
throw new RuntimeError(`Cannot index ${describeType(container)} with string`, node.span);
|
|
1740
|
+
}
|
|
1741
|
+
};
|
|
1742
|
+
const evalIndex = function* (node, input, env, tracker) {
|
|
1743
|
+
const indexValues = Array.from(evaluate(node.index, input, env, tracker));
|
|
1744
|
+
for (const container of evaluate(node.target, input, env, tracker)) {
|
|
1745
|
+
if (container === null) {
|
|
1746
|
+
yield emit(null, node.span, tracker);
|
|
1747
|
+
continue;
|
|
1748
|
+
}
|
|
1749
|
+
if (isValueArray(container)) {
|
|
1750
|
+
for (const idxValue of indexValues) {
|
|
1751
|
+
const index = ensureInteger(idxValue, node.span);
|
|
1752
|
+
const resolved = index < 0 ? container.length + index : index;
|
|
1753
|
+
if (resolved < 0 || resolved >= container.length) yield emit(null, node.span, tracker);
|
|
1754
|
+
else yield emit(container[resolved], node.span, tracker);
|
|
1755
|
+
}
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
if (isPlainObject(container)) {
|
|
1759
|
+
for (const keyValue of indexValues) {
|
|
1760
|
+
if (typeof keyValue !== "string") throw new RuntimeError(`Cannot index object with ${describeType(keyValue)}`, node.span);
|
|
1761
|
+
yield emit(Object.prototype.hasOwnProperty.call(container, keyValue) ? container[keyValue] : null, node.span, tracker);
|
|
1762
|
+
}
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
throw new RuntimeError(`Cannot index ${describeType(container)}`, node.span);
|
|
1766
|
+
}
|
|
1767
|
+
};
|
|
1768
|
+
const evalIterate = function* (node, input, env, tracker) {
|
|
1769
|
+
for (const container of evaluate(node.target, input, env, tracker)) {
|
|
1770
|
+
if (container === null) continue;
|
|
1771
|
+
if (isValueArray(container)) {
|
|
1772
|
+
for (const item of container) yield emit(item, node.span, tracker);
|
|
1773
|
+
continue;
|
|
1774
|
+
}
|
|
1775
|
+
if (isPlainObject(container)) {
|
|
1776
|
+
const keys = Object.keys(container).sort();
|
|
1777
|
+
for (const key of keys) yield emit(container[key], node.span, tracker);
|
|
1778
|
+
continue;
|
|
1779
|
+
}
|
|
1780
|
+
throw new RuntimeError(`Cannot iterate over ${describeType(container)}`, node.span);
|
|
1781
|
+
}
|
|
1782
|
+
};
|
|
1783
|
+
const evalReduce = function* (node, input, env, tracker) {
|
|
1784
|
+
const initValues = Array.from(evaluate(node.init, input, env, tracker));
|
|
1785
|
+
if (initValues.length !== 1) throw new RuntimeError("Reduce init must single value", node.init.span);
|
|
1786
|
+
let acc = initValues[0];
|
|
1787
|
+
for (const item of evaluate(node.source, input, env, tracker)) {
|
|
1788
|
+
tracker.step(node.span);
|
|
1789
|
+
pushBinding(env, node.var, item);
|
|
1790
|
+
try {
|
|
1791
|
+
const updates = Array.from(evaluate(node.update, acc, env, tracker));
|
|
1792
|
+
if (updates.length !== 1) throw new RuntimeError("Reduce update must produce single value", node.update.span);
|
|
1793
|
+
acc = updates[0];
|
|
1794
|
+
} finally {
|
|
1795
|
+
popBinding(env);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
yield emit(acc, node.span, tracker);
|
|
1799
|
+
};
|
|
1800
|
+
const evalForeach = function* (node, input, env, tracker) {
|
|
1801
|
+
const initValues = Array.from(evaluate(node.init, input, env, tracker));
|
|
1802
|
+
if (initValues.length !== 1) throw new RuntimeError("Foreach init must single value", node.init.span);
|
|
1803
|
+
let acc = initValues[0];
|
|
1804
|
+
for (const item of evaluate(node.source, input, env, tracker)) {
|
|
1805
|
+
tracker.step(node.span);
|
|
1806
|
+
pushBinding(env, node.var, item);
|
|
1807
|
+
try {
|
|
1808
|
+
const updates = Array.from(evaluate(node.update, acc, env, tracker));
|
|
1809
|
+
if (updates.length !== 1) throw new RuntimeError("Foreach update must produce single value", node.update.span);
|
|
1810
|
+
acc = updates[0];
|
|
1811
|
+
if (node.extract) for (const extracted of evaluate(node.extract, acc, env, tracker)) yield emit(extracted, node.span, tracker);
|
|
1812
|
+
else yield emit(acc, node.span, tracker);
|
|
1813
|
+
} finally {
|
|
1814
|
+
popBinding(env);
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
};
|
|
1818
|
+
const evalTry = function* (node, input, env, tracker) {
|
|
1819
|
+
try {
|
|
1820
|
+
yield* evaluate(node.body, input, env, tracker);
|
|
1821
|
+
} catch (err) {
|
|
1822
|
+
if (err instanceof RuntimeError) {
|
|
1823
|
+
if (node.handler) yield* evaluate(node.handler, err.message, env, tracker);
|
|
1824
|
+
} else throw err;
|
|
1825
|
+
}
|
|
1826
|
+
};
|
|
1827
|
+
const evalRecurse = function* (node, input, env, tracker) {
|
|
1828
|
+
yield emit(input, node.span, tracker);
|
|
1829
|
+
tracker.step(node.span);
|
|
1830
|
+
if (isValueArray(input)) for (const item of input) yield* evaluate(node, item, env, tracker);
|
|
1831
|
+
else if (isPlainObject(input)) {
|
|
1832
|
+
const keys = Object.keys(input).sort();
|
|
1833
|
+
for (const key of keys) yield* evaluate(node, input[key], env, tracker);
|
|
1834
|
+
}
|
|
1835
|
+
};
|
|
1836
|
+
const buildArray = (node, input, env, tracker) => {
|
|
1837
|
+
const result = [];
|
|
1838
|
+
node.items.forEach((item) => {
|
|
1839
|
+
for (const value of evaluate(item, input, env, tracker)) result.push(value);
|
|
1840
|
+
});
|
|
1841
|
+
return result;
|
|
1842
|
+
};
|
|
1843
|
+
const buildObjects = function* (node, input, env, tracker) {
|
|
1844
|
+
if (node.entries.length === 0) {
|
|
1845
|
+
yield emit({}, node.span, tracker);
|
|
1846
|
+
return;
|
|
1847
|
+
}
|
|
1848
|
+
let partials = [{}];
|
|
1849
|
+
for (const entry of node.entries) {
|
|
1850
|
+
const keys = resolveObjectKeys(entry.key, input, env, tracker);
|
|
1851
|
+
const values = Array.from(evaluate(entry.value, input, env, tracker));
|
|
1852
|
+
const next = [];
|
|
1853
|
+
partials.forEach((partial) => {
|
|
1854
|
+
keys.forEach((key) => {
|
|
1855
|
+
values.forEach((value) => {
|
|
1856
|
+
next.push(extendRecord(partial, key, value));
|
|
1857
|
+
});
|
|
1858
|
+
});
|
|
1859
|
+
});
|
|
1860
|
+
partials = next;
|
|
1861
|
+
if (partials.length === 0) return;
|
|
1862
|
+
}
|
|
1863
|
+
for (const obj of partials) yield emit(obj, node.span, tracker);
|
|
1864
|
+
};
|
|
1865
|
+
const resolveObjectKeys = (key, input, env, tracker) => {
|
|
1866
|
+
if (key.kind === "KeyIdentifier") return [key.name];
|
|
1867
|
+
if (key.kind === "KeyString") return [key.value];
|
|
1868
|
+
return Array.from(evaluate(key.expr, input, env, tracker)).map((value) => {
|
|
1869
|
+
if (typeof value !== "string") throw new RuntimeError("Object key expression must produce strings", key.span);
|
|
1870
|
+
return value;
|
|
1871
|
+
});
|
|
1872
|
+
};
|
|
1873
|
+
const evalBinary = function* (node, input, env, tracker) {
|
|
1874
|
+
const leftValues = Array.from(evaluate(node.left, input, env, tracker));
|
|
1875
|
+
const rightValues = Array.from(evaluate(node.right, input, env, tracker));
|
|
1876
|
+
for (const left of leftValues) for (const right of rightValues) yield emit(applyBinaryOp(node.op, left, right, node.span), node.span, tracker);
|
|
1877
|
+
};
|
|
1878
|
+
const applyBinaryOp = (op, left, right, span) => {
|
|
1879
|
+
switch (op) {
|
|
1880
|
+
case "+": return applyPlus(left, right, span);
|
|
1881
|
+
case "-": return applyMinus(left, right, span);
|
|
1882
|
+
case "*": return applyMultiply(left, right, span);
|
|
1883
|
+
case "/": return applyDivide(left, right, span);
|
|
1884
|
+
case "%": return applyModulo(left, right, span);
|
|
1885
|
+
case "Eq": return valueEquals(left, right);
|
|
1886
|
+
case "Neq": return !valueEquals(left, right);
|
|
1887
|
+
case "Lt": return compareValues(left, right) < 0;
|
|
1888
|
+
case "Lte": return compareValues(left, right) <= 0;
|
|
1889
|
+
case "Gt": return compareValues(left, right) > 0;
|
|
1890
|
+
case "Gte": return compareValues(left, right) >= 0;
|
|
1891
|
+
default: throw new RuntimeError(`Unsupported operator ${op}`, span);
|
|
1892
|
+
}
|
|
1893
|
+
};
|
|
1894
|
+
const applyUnaryNeg = (value, span) => {
|
|
1895
|
+
if (typeof value === "number") return -value;
|
|
1896
|
+
throw new RuntimeError("Unary \"-\" expects a number", span);
|
|
1897
|
+
};
|
|
1898
|
+
const applyPlus = (left, right, span) => {
|
|
1899
|
+
if (left === null) return cloneValue(right);
|
|
1900
|
+
if (right === null) return cloneValue(left);
|
|
1901
|
+
if (typeof left === "number" && typeof right === "number") return left + right;
|
|
1902
|
+
if (typeof left === "string" && typeof right === "string") return left + right;
|
|
1903
|
+
if (isValueArray(left) && isValueArray(right)) return [...left, ...right];
|
|
1904
|
+
if (isPlainObject(left) && isPlainObject(right)) return mergeShallowObjects(left, right);
|
|
1905
|
+
throw new RuntimeError(`Cannot add ${describeType(left)} and ${describeType(right)}`, span);
|
|
1906
|
+
};
|
|
1907
|
+
const applyMinus = (left, right, span) => {
|
|
1908
|
+
if (typeof left === "number" && typeof right === "number") return left - right;
|
|
1909
|
+
if (isValueArray(left) && isValueArray(right)) return left.filter((item) => !right.some((candidate) => valueEquals(item, candidate)));
|
|
1910
|
+
throw new RuntimeError(`Cannot subtract ${describeType(right)} from ${describeType(left)}`, span);
|
|
1911
|
+
};
|
|
1912
|
+
const applyMultiply = (left, right, span) => {
|
|
1913
|
+
if (typeof left === "number" && typeof right === "number") return left * right;
|
|
1914
|
+
if (typeof left === "string" && typeof right === "number") return repeatString(left, right, span);
|
|
1915
|
+
if (typeof right === "string" && typeof left === "number") return repeatString(right, left, span);
|
|
1916
|
+
if (isPlainObject(left) && isPlainObject(right)) return deepMergeObjects(left, right);
|
|
1917
|
+
throw new RuntimeError(`Cannot multiply ${describeType(left)} and ${describeType(right)}`, span);
|
|
1918
|
+
};
|
|
1919
|
+
const applyDivide = (left, right, span) => {
|
|
1920
|
+
if (typeof left !== "number" || typeof right !== "number") throw new RuntimeError("Division expects two numbers", span);
|
|
1921
|
+
if (right === 0) throw new RuntimeError("Division by zero", span);
|
|
1922
|
+
return left / right;
|
|
1923
|
+
};
|
|
1924
|
+
const applyModulo = (left, right, span) => {
|
|
1925
|
+
if (typeof left !== "number" || typeof right !== "number") throw new RuntimeError("Modulo expects two numbers", span);
|
|
1926
|
+
if (right === 0) throw new RuntimeError("Modulo by zero", span);
|
|
1927
|
+
return left % right;
|
|
1928
|
+
};
|
|
1929
|
+
const repeatString = (text, countValue, span) => {
|
|
1930
|
+
const count = ensureInteger(countValue, span);
|
|
1931
|
+
if (count < 0) throw new RuntimeError("String repeat expects non-negative count", span);
|
|
1932
|
+
return text.repeat(count);
|
|
1933
|
+
};
|
|
1934
|
+
const deepMergeObjects = (left, right) => {
|
|
1935
|
+
const result = {};
|
|
1936
|
+
Object.keys(left).forEach((key) => {
|
|
1937
|
+
const leftValue = left[key];
|
|
1938
|
+
if (leftValue !== void 0) result[key] = cloneValue(leftValue);
|
|
1939
|
+
});
|
|
1940
|
+
Object.keys(right).forEach((key) => {
|
|
1941
|
+
const existing = result[key];
|
|
1942
|
+
const rightValue = right[key];
|
|
1943
|
+
if (rightValue === void 0) return;
|
|
1944
|
+
if (existing !== void 0 && isPlainObject(existing) && isPlainObject(rightValue)) result[key] = deepMergeObjects(existing, rightValue);
|
|
1945
|
+
else result[key] = cloneValue(rightValue);
|
|
1946
|
+
});
|
|
1947
|
+
return result;
|
|
1948
|
+
};
|
|
1949
|
+
const cloneValue = (value) => {
|
|
1950
|
+
if (isValueArray(value)) return value.map((item) => cloneValue(item));
|
|
1951
|
+
if (isPlainObject(value)) {
|
|
1952
|
+
const result = {};
|
|
1953
|
+
Object.keys(value).forEach((key) => {
|
|
1954
|
+
const child = value[key];
|
|
1955
|
+
if (child !== void 0) result[key] = cloneValue(child);
|
|
1956
|
+
});
|
|
1957
|
+
return result;
|
|
1958
|
+
}
|
|
1959
|
+
return value;
|
|
1960
|
+
};
|
|
1961
|
+
const getVar = (env, name, span) => {
|
|
1962
|
+
for (let i = env.length - 1; i >= 0; i -= 1) {
|
|
1963
|
+
const frame = env[i];
|
|
1964
|
+
if (frame && frame.has(name)) return frame.get(name);
|
|
1965
|
+
}
|
|
1966
|
+
throw new RuntimeError(`Unbound variable: $${name}`, span);
|
|
1967
|
+
};
|
|
1968
|
+
const pushBinding = (env, name, value) => {
|
|
1969
|
+
env.push(new Map([[name, value]]));
|
|
1970
|
+
};
|
|
1971
|
+
const popBinding = (env) => {
|
|
1972
|
+
env.pop();
|
|
1973
|
+
};
|
|
1974
|
+
const ensureInteger = (value, span) => {
|
|
1975
|
+
if (typeof value !== "number" || !Number.isInteger(value)) throw new RuntimeError("Index must be an integer number", span);
|
|
1976
|
+
return value;
|
|
1977
|
+
};
|
|
1978
|
+
const extendRecord = (base, key, value) => {
|
|
1979
|
+
const next = {};
|
|
1980
|
+
Object.keys(base).forEach((existingKey) => {
|
|
1981
|
+
const existingValue = base[existingKey];
|
|
1982
|
+
if (existingValue !== void 0) next[existingKey] = existingValue;
|
|
1983
|
+
});
|
|
1984
|
+
next[key] = value;
|
|
1985
|
+
return next;
|
|
1986
|
+
};
|
|
1987
|
+
const mergeShallowObjects = (left, right) => {
|
|
1988
|
+
const result = {};
|
|
1989
|
+
Object.keys(left).forEach((key) => {
|
|
1990
|
+
const leftValue = left[key];
|
|
1991
|
+
if (leftValue !== void 0) result[key] = leftValue;
|
|
1992
|
+
});
|
|
1993
|
+
Object.keys(right).forEach((key) => {
|
|
1994
|
+
const rightValue = right[key];
|
|
1995
|
+
if (rightValue !== void 0) result[key] = rightValue;
|
|
1996
|
+
});
|
|
1997
|
+
return result;
|
|
1998
|
+
};
|
|
1999
|
+
const isValueArray = (value) => Array.isArray(value);
|
|
2000
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2001
|
+
const describeType = (value) => {
|
|
2002
|
+
if (value === null) return "null";
|
|
2003
|
+
if (typeof value === "boolean") return "boolean";
|
|
2004
|
+
if (typeof value === "number") return "number";
|
|
2005
|
+
if (typeof value === "string") return "string";
|
|
2006
|
+
if (Array.isArray(value)) return "array";
|
|
2007
|
+
return "object";
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
//#endregion
|
|
2011
|
+
//#region src/index.ts
|
|
2012
|
+
/**
|
|
2013
|
+
* Runs a jq query against a JSON input.
|
|
2014
|
+
*
|
|
2015
|
+
* @param source - The jq query string (e.g., `.foo | .bar`).
|
|
2016
|
+
* @param input - The JSON input value (object, array, string, number, boolean, or null).
|
|
2017
|
+
* @param options - Execution options including limits.
|
|
2018
|
+
* @returns An array of results. jq queries always produce zero or more values.
|
|
2019
|
+
* @throws {LexError} If the query contains invalid characters.
|
|
2020
|
+
* @throws {ParseError} If the query syntax is invalid.
|
|
2021
|
+
* @throws {ValidationError} If the query uses unsupported features.
|
|
2022
|
+
* @throws {RuntimeError} If execution fails (e.g., type error) or exceeds limits.
|
|
2023
|
+
*/
|
|
2024
|
+
const run = (source, input, options = {}) => {
|
|
2025
|
+
const ast = parse(source);
|
|
2026
|
+
validate(ast);
|
|
2027
|
+
return runAst(ast, input, options);
|
|
2028
|
+
};
|
|
2029
|
+
|
|
2030
|
+
//#endregion
|
|
2031
|
+
exports.LexError = LexError;
|
|
2032
|
+
exports.LimitTracker = LimitTracker;
|
|
2033
|
+
exports.ParseError = ParseError;
|
|
2034
|
+
exports.RuntimeError = RuntimeError;
|
|
2035
|
+
exports.ValidationError = ValidationError;
|
|
2036
|
+
exports.parse = parse;
|
|
2037
|
+
exports.resolveLimits = resolveLimits;
|
|
2038
|
+
exports.run = run;
|
|
2039
|
+
exports.runAst = runAst;
|
|
2040
|
+
exports.validate = validate;
|