@bpmnkit/feel 0.0.8
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/README.md +112 -0
- package/dist/ast.d.ts +111 -0
- package/dist/ast.js +2 -0
- package/dist/builtins.d.ts +10 -0
- package/dist/builtins.js +1194 -0
- package/dist/evaluator.d.ts +13 -0
- package/dist/evaluator.js +586 -0
- package/dist/formatter.d.ts +7 -0
- package/dist/formatter.js +120 -0
- package/dist/highlighter.d.ts +14 -0
- package/dist/highlighter.js +149 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +12 -0
- package/dist/lexer.d.ts +9 -0
- package/dist/lexer.js +161 -0
- package/dist/parser.d.ts +13 -0
- package/dist/parser.js +853 -0
- package/dist/types.d.ts +56 -0
- package/dist/types.js +129 -0
- package/package.json +29 -0
package/dist/parser.js
ADDED
|
@@ -0,0 +1,853 @@
|
|
|
1
|
+
import { tokenize } from "./lexer.js";
|
|
2
|
+
// All known multi-word built-in names
|
|
3
|
+
const BUILTIN_NAMES = new Set([
|
|
4
|
+
"string length",
|
|
5
|
+
"upper case",
|
|
6
|
+
"lower case",
|
|
7
|
+
"substring before",
|
|
8
|
+
"substring after",
|
|
9
|
+
"string join",
|
|
10
|
+
"list contains",
|
|
11
|
+
"insert before",
|
|
12
|
+
"index of",
|
|
13
|
+
"distinct values",
|
|
14
|
+
"get value",
|
|
15
|
+
"get entries",
|
|
16
|
+
"context put",
|
|
17
|
+
"context merge",
|
|
18
|
+
"is defined",
|
|
19
|
+
"day of week",
|
|
20
|
+
"day of year",
|
|
21
|
+
"week of year",
|
|
22
|
+
"month of year",
|
|
23
|
+
"last day of month",
|
|
24
|
+
"years and months duration",
|
|
25
|
+
"date and time",
|
|
26
|
+
"round half up",
|
|
27
|
+
"round half down",
|
|
28
|
+
"round up",
|
|
29
|
+
"round down",
|
|
30
|
+
"get or else",
|
|
31
|
+
"met by",
|
|
32
|
+
"overlaps before",
|
|
33
|
+
"overlaps after",
|
|
34
|
+
"started by",
|
|
35
|
+
"finished by",
|
|
36
|
+
"random number",
|
|
37
|
+
"starts with",
|
|
38
|
+
"ends with",
|
|
39
|
+
]);
|
|
40
|
+
// All strict prefixes of multi-word built-in names
|
|
41
|
+
const BUILTIN_PREFIXES = (() => {
|
|
42
|
+
const s = new Set();
|
|
43
|
+
for (const name of BUILTIN_NAMES) {
|
|
44
|
+
const parts = name.split(" ");
|
|
45
|
+
for (let i = 1; i < parts.length; i++) {
|
|
46
|
+
s.add(parts.slice(0, i).join(" "));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return s;
|
|
50
|
+
})();
|
|
51
|
+
class Parser {
|
|
52
|
+
tokens;
|
|
53
|
+
pos = 0;
|
|
54
|
+
errors = [];
|
|
55
|
+
constructor(input) {
|
|
56
|
+
this.tokens = tokenize(input).filter((t) => t.kind !== "whitespace" && t.kind !== "comment");
|
|
57
|
+
}
|
|
58
|
+
peek(offset = 0) {
|
|
59
|
+
return this.tokens[this.pos + offset];
|
|
60
|
+
}
|
|
61
|
+
advance() {
|
|
62
|
+
const tok = this.tokens[this.pos];
|
|
63
|
+
if (tok)
|
|
64
|
+
this.pos++;
|
|
65
|
+
return tok;
|
|
66
|
+
}
|
|
67
|
+
check(kind, value) {
|
|
68
|
+
const tok = this.peek();
|
|
69
|
+
if (!tok || tok.kind !== kind)
|
|
70
|
+
return false;
|
|
71
|
+
return value === undefined || tok.value === value;
|
|
72
|
+
}
|
|
73
|
+
consume(kind, value) {
|
|
74
|
+
if (!this.check(kind, value))
|
|
75
|
+
return undefined;
|
|
76
|
+
return this.advance();
|
|
77
|
+
}
|
|
78
|
+
expect(kind, value) {
|
|
79
|
+
const tok = this.consume(kind, value);
|
|
80
|
+
if (!tok) {
|
|
81
|
+
const cur = this.peek();
|
|
82
|
+
const pos = cur ? cur.start : (this.tokens[this.tokens.length - 1]?.end ?? 0);
|
|
83
|
+
const label = value ?? kind;
|
|
84
|
+
this.errors.push({ message: `Expected ${label}`, start: pos, end: pos + 1 });
|
|
85
|
+
}
|
|
86
|
+
return tok;
|
|
87
|
+
}
|
|
88
|
+
/** Try to extend a single name token into a multi-word built-in name. */
|
|
89
|
+
resolveMultiwordName(first) {
|
|
90
|
+
let name = first;
|
|
91
|
+
while (true) {
|
|
92
|
+
// Only try to extend if current is a known prefix
|
|
93
|
+
if (!BUILTIN_PREFIXES.has(name))
|
|
94
|
+
break;
|
|
95
|
+
const next = this.peek();
|
|
96
|
+
if (!next || (next.kind !== "name" && next.kind !== "keyword"))
|
|
97
|
+
break;
|
|
98
|
+
const extended = `${name} ${next.value}`;
|
|
99
|
+
if (BUILTIN_NAMES.has(extended) || BUILTIN_PREFIXES.has(extended)) {
|
|
100
|
+
this.advance();
|
|
101
|
+
name = extended;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return name;
|
|
108
|
+
}
|
|
109
|
+
// -------------------------------------------------------------------------
|
|
110
|
+
// Expression parsing (Pratt)
|
|
111
|
+
// -------------------------------------------------------------------------
|
|
112
|
+
parseExpression(minPrec = 0) {
|
|
113
|
+
const tok = this.peek();
|
|
114
|
+
if (!tok)
|
|
115
|
+
return null;
|
|
116
|
+
let left = this.parsePrefix();
|
|
117
|
+
if (!left)
|
|
118
|
+
return null;
|
|
119
|
+
while (true) {
|
|
120
|
+
const prec = this.infixPrec();
|
|
121
|
+
if (prec <= minPrec)
|
|
122
|
+
break;
|
|
123
|
+
const next = this.parseInfix(left, prec);
|
|
124
|
+
if (!next)
|
|
125
|
+
break;
|
|
126
|
+
left = next;
|
|
127
|
+
}
|
|
128
|
+
return left;
|
|
129
|
+
}
|
|
130
|
+
infixPrec() {
|
|
131
|
+
const tok = this.peek();
|
|
132
|
+
if (!tok)
|
|
133
|
+
return 0;
|
|
134
|
+
if (tok.kind === "keyword" && tok.value === "or")
|
|
135
|
+
return 10;
|
|
136
|
+
if (tok.kind === "keyword" && tok.value === "and")
|
|
137
|
+
return 20;
|
|
138
|
+
if (tok.kind === "keyword" && tok.value === "between")
|
|
139
|
+
return 30;
|
|
140
|
+
if (tok.kind === "keyword" && tok.value === "in")
|
|
141
|
+
return 30;
|
|
142
|
+
if (tok.kind === "keyword" && tok.value === "instance")
|
|
143
|
+
return 30;
|
|
144
|
+
if (tok.kind === "op") {
|
|
145
|
+
if (tok.value === "=" ||
|
|
146
|
+
tok.value === "!=" ||
|
|
147
|
+
tok.value === "<" ||
|
|
148
|
+
tok.value === "<=" ||
|
|
149
|
+
tok.value === ">" ||
|
|
150
|
+
tok.value === ">=")
|
|
151
|
+
return 30;
|
|
152
|
+
if (tok.value === "+" || tok.value === "-")
|
|
153
|
+
return 40;
|
|
154
|
+
if (tok.value === "*" || tok.value === "/")
|
|
155
|
+
return 50;
|
|
156
|
+
if (tok.value === "**")
|
|
157
|
+
return 60;
|
|
158
|
+
}
|
|
159
|
+
if (tok.kind === "punct" && tok.value === ".")
|
|
160
|
+
return 80;
|
|
161
|
+
if (tok.kind === "punct" && tok.value === "[")
|
|
162
|
+
return 80;
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
parseInfix(left, prec) {
|
|
166
|
+
const tok = this.peek();
|
|
167
|
+
if (!tok)
|
|
168
|
+
return null;
|
|
169
|
+
// Binary operators: or, and
|
|
170
|
+
if (tok.kind === "keyword" && (tok.value === "or" || tok.value === "and")) {
|
|
171
|
+
this.advance();
|
|
172
|
+
const op = tok.value;
|
|
173
|
+
const right = this.parseExpression(prec - (op === "**" ? 1 : 0));
|
|
174
|
+
if (!right)
|
|
175
|
+
return null;
|
|
176
|
+
return { kind: "binary", op, left, right, start: left.start, end: right.end };
|
|
177
|
+
}
|
|
178
|
+
// Comparison operators
|
|
179
|
+
if (tok.kind === "op" &&
|
|
180
|
+
(tok.value === "=" ||
|
|
181
|
+
tok.value === "!=" ||
|
|
182
|
+
tok.value === "<" ||
|
|
183
|
+
tok.value === "<=" ||
|
|
184
|
+
tok.value === ">" ||
|
|
185
|
+
tok.value === ">=")) {
|
|
186
|
+
this.advance();
|
|
187
|
+
const op = tok.value;
|
|
188
|
+
const right = this.parseExpression(prec);
|
|
189
|
+
if (!right)
|
|
190
|
+
return null;
|
|
191
|
+
return { kind: "binary", op, left, right, start: left.start, end: right.end };
|
|
192
|
+
}
|
|
193
|
+
// Arithmetic
|
|
194
|
+
if (tok.kind === "op" &&
|
|
195
|
+
(tok.value === "+" ||
|
|
196
|
+
tok.value === "-" ||
|
|
197
|
+
tok.value === "*" ||
|
|
198
|
+
tok.value === "/" ||
|
|
199
|
+
tok.value === "**")) {
|
|
200
|
+
this.advance();
|
|
201
|
+
const op = tok.value;
|
|
202
|
+
// ** is right-associative
|
|
203
|
+
const rightPrec = op === "**" ? prec - 1 : prec;
|
|
204
|
+
const right = this.parseExpression(rightPrec);
|
|
205
|
+
if (!right) {
|
|
206
|
+
const pos = tok.end;
|
|
207
|
+
this.errors.push({
|
|
208
|
+
message: `Expected expression after '${op}'`,
|
|
209
|
+
start: pos,
|
|
210
|
+
end: pos + 1,
|
|
211
|
+
});
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return { kind: "binary", op, left, right, start: left.start, end: right.end };
|
|
215
|
+
}
|
|
216
|
+
// between
|
|
217
|
+
if (tok.kind === "keyword" && tok.value === "between") {
|
|
218
|
+
this.advance();
|
|
219
|
+
const low = this.parseExpression(40); // above + -
|
|
220
|
+
if (!low)
|
|
221
|
+
return null;
|
|
222
|
+
if (!this.expect("keyword", "and"))
|
|
223
|
+
return null;
|
|
224
|
+
const high = this.parseExpression(40);
|
|
225
|
+
if (!high)
|
|
226
|
+
return null;
|
|
227
|
+
return { kind: "between", value: left, low, high, start: left.start, end: high.end };
|
|
228
|
+
}
|
|
229
|
+
// in
|
|
230
|
+
if (tok.kind === "keyword" && tok.value === "in") {
|
|
231
|
+
this.advance();
|
|
232
|
+
const test = this.parseInTestExpr();
|
|
233
|
+
if (!test)
|
|
234
|
+
return null;
|
|
235
|
+
return { kind: "in-test", value: left, test, start: left.start, end: test.end };
|
|
236
|
+
}
|
|
237
|
+
// instance of
|
|
238
|
+
if (tok.kind === "keyword" && tok.value === "instance") {
|
|
239
|
+
this.advance();
|
|
240
|
+
if (!this.expect("keyword", "of"))
|
|
241
|
+
return null;
|
|
242
|
+
const typeName = this.parseTypeName();
|
|
243
|
+
if (!typeName)
|
|
244
|
+
return null;
|
|
245
|
+
return {
|
|
246
|
+
kind: "instance-of",
|
|
247
|
+
value: left,
|
|
248
|
+
typeName,
|
|
249
|
+
start: left.start,
|
|
250
|
+
end: this.pos > 0 ? (this.tokens[this.pos - 1]?.end ?? left.end) : left.end,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// Path access: expr.name
|
|
254
|
+
if (tok.kind === "punct" && tok.value === ".") {
|
|
255
|
+
this.advance();
|
|
256
|
+
const nameTok = this.peek();
|
|
257
|
+
if (!nameTok || (nameTok.kind !== "name" && nameTok.kind !== "keyword")) {
|
|
258
|
+
this.errors.push({ message: "Expected name after '.'", start: tok.start, end: tok.end });
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
this.advance();
|
|
262
|
+
return { kind: "path", base: left, key: nameTok.value, start: left.start, end: nameTok.end };
|
|
263
|
+
}
|
|
264
|
+
// Filter: expr[condition]
|
|
265
|
+
if (tok.kind === "punct" && tok.value === "[") {
|
|
266
|
+
this.advance();
|
|
267
|
+
const cond = this.parseExpression(0);
|
|
268
|
+
if (!cond)
|
|
269
|
+
return null;
|
|
270
|
+
const close = this.expect("punct", "]");
|
|
271
|
+
return {
|
|
272
|
+
kind: "filter",
|
|
273
|
+
base: left,
|
|
274
|
+
condition: cond,
|
|
275
|
+
start: left.start,
|
|
276
|
+
end: close?.end ?? cond.end,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
parsePrefix() {
|
|
282
|
+
const tok = this.peek();
|
|
283
|
+
if (!tok)
|
|
284
|
+
return null;
|
|
285
|
+
// Unary minus
|
|
286
|
+
if (tok.kind === "op" && tok.value === "-") {
|
|
287
|
+
this.advance();
|
|
288
|
+
const operand = this.parseExpression(70);
|
|
289
|
+
if (!operand)
|
|
290
|
+
return null;
|
|
291
|
+
return { kind: "unary-minus", operand, start: tok.start, end: operand.end };
|
|
292
|
+
}
|
|
293
|
+
// Number literal
|
|
294
|
+
if (tok.kind === "number") {
|
|
295
|
+
this.advance();
|
|
296
|
+
return { kind: "number", value: Number(tok.value), start: tok.start, end: tok.end };
|
|
297
|
+
}
|
|
298
|
+
// String literal
|
|
299
|
+
if (tok.kind === "string") {
|
|
300
|
+
this.advance();
|
|
301
|
+
const raw = tok.value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
|
|
302
|
+
return { kind: "string", value: raw, start: tok.start, end: tok.end };
|
|
303
|
+
}
|
|
304
|
+
// Temporal literal
|
|
305
|
+
if (tok.kind === "temporal") {
|
|
306
|
+
this.advance();
|
|
307
|
+
return { kind: "temporal", raw: tok.value, start: tok.start, end: tok.end };
|
|
308
|
+
}
|
|
309
|
+
// Backtick name
|
|
310
|
+
if (tok.kind === "backtick") {
|
|
311
|
+
this.advance();
|
|
312
|
+
return { kind: "name", name: tok.value, start: tok.start, end: tok.end };
|
|
313
|
+
}
|
|
314
|
+
// Boolean / null keywords
|
|
315
|
+
if (tok.kind === "keyword") {
|
|
316
|
+
if (tok.value === "true") {
|
|
317
|
+
this.advance();
|
|
318
|
+
return { kind: "boolean", value: true, start: tok.start, end: tok.end };
|
|
319
|
+
}
|
|
320
|
+
if (tok.value === "false") {
|
|
321
|
+
this.advance();
|
|
322
|
+
return { kind: "boolean", value: false, start: tok.start, end: tok.end };
|
|
323
|
+
}
|
|
324
|
+
if (tok.value === "null") {
|
|
325
|
+
this.advance();
|
|
326
|
+
return { kind: "null", start: tok.start, end: tok.end };
|
|
327
|
+
}
|
|
328
|
+
// if-then-else
|
|
329
|
+
if (tok.value === "if")
|
|
330
|
+
return this.parseIf();
|
|
331
|
+
// for
|
|
332
|
+
if (tok.value === "for")
|
|
333
|
+
return this.parseFor();
|
|
334
|
+
// some / every
|
|
335
|
+
if (tok.value === "some" || tok.value === "every")
|
|
336
|
+
return this.parseQuantifier(tok.value);
|
|
337
|
+
// function
|
|
338
|
+
if (tok.value === "function")
|
|
339
|
+
return this.parseFunctionDef();
|
|
340
|
+
// not(...) — could be negation or not() built-in
|
|
341
|
+
if (tok.value === "not")
|
|
342
|
+
return this.parseNot();
|
|
343
|
+
}
|
|
344
|
+
// Name or function call
|
|
345
|
+
if (tok.kind === "name") {
|
|
346
|
+
this.advance();
|
|
347
|
+
const name = this.resolveMultiwordName(tok.value);
|
|
348
|
+
return this.parseNameOrCall(name, tok.start);
|
|
349
|
+
}
|
|
350
|
+
// Grouped expression or range open
|
|
351
|
+
if (tok.kind === "punct" && tok.value === "(") {
|
|
352
|
+
return this.parseParenOrRange();
|
|
353
|
+
}
|
|
354
|
+
// List
|
|
355
|
+
if (tok.kind === "punct" && tok.value === "[") {
|
|
356
|
+
return this.parseListOrRange();
|
|
357
|
+
}
|
|
358
|
+
// Context
|
|
359
|
+
if (tok.kind === "punct" && tok.value === "{") {
|
|
360
|
+
return this.parseContext();
|
|
361
|
+
}
|
|
362
|
+
// ? — implicit input for unary test mode (also valid in expression as input reference)
|
|
363
|
+
if (tok.kind === "op" && tok.value === "?") {
|
|
364
|
+
this.advance();
|
|
365
|
+
return { kind: "name", name: "?", start: tok.start, end: tok.end };
|
|
366
|
+
}
|
|
367
|
+
this.errors.push({
|
|
368
|
+
message: `Unexpected token '${tok.value}'`,
|
|
369
|
+
start: tok.start,
|
|
370
|
+
end: tok.end,
|
|
371
|
+
});
|
|
372
|
+
this.advance(); // skip for error recovery
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
// -------------------------------------------------------------------------
|
|
376
|
+
// Helpers
|
|
377
|
+
// -------------------------------------------------------------------------
|
|
378
|
+
parseNameOrCall(name, start) {
|
|
379
|
+
// Check for function call: name(args)
|
|
380
|
+
if (this.check("punct", "(")) {
|
|
381
|
+
this.advance(); // consume (
|
|
382
|
+
// Check for named args: name: value pattern
|
|
383
|
+
if (this.isNamedArgList()) {
|
|
384
|
+
return this.parseNamedCall(name, start);
|
|
385
|
+
}
|
|
386
|
+
const args = [];
|
|
387
|
+
if (!this.check("punct", ")")) {
|
|
388
|
+
const arg = this.parseExpression(0);
|
|
389
|
+
if (arg)
|
|
390
|
+
args.push(arg);
|
|
391
|
+
while (this.consume("punct", ",")) {
|
|
392
|
+
const a = this.parseExpression(0);
|
|
393
|
+
if (a)
|
|
394
|
+
args.push(a);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const close = this.expect("punct", ")");
|
|
398
|
+
return {
|
|
399
|
+
kind: "call",
|
|
400
|
+
callee: name,
|
|
401
|
+
args,
|
|
402
|
+
start,
|
|
403
|
+
end: close?.end ?? args[args.length - 1]?.end ?? start,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
return { kind: "name", name, start, end: this.tokens[this.pos - 1]?.end ?? start };
|
|
407
|
+
}
|
|
408
|
+
isNamedArgList() {
|
|
409
|
+
// Peek: name colon value pattern?
|
|
410
|
+
const t0 = this.peek(0);
|
|
411
|
+
const t1 = this.peek(1);
|
|
412
|
+
return !!(t0 &&
|
|
413
|
+
(t0.kind === "name" || t0.kind === "keyword") &&
|
|
414
|
+
t1 &&
|
|
415
|
+
t1.kind === "punct" &&
|
|
416
|
+
t1.value === ":");
|
|
417
|
+
}
|
|
418
|
+
parseNamedCall(callee, start) {
|
|
419
|
+
const args = [];
|
|
420
|
+
if (!this.check("punct", ")")) {
|
|
421
|
+
const parsePair = () => {
|
|
422
|
+
const nameTok = this.advance();
|
|
423
|
+
if (!nameTok)
|
|
424
|
+
return false;
|
|
425
|
+
if (!this.expect("punct", ":"))
|
|
426
|
+
return false;
|
|
427
|
+
const val = this.parseExpression(0);
|
|
428
|
+
if (!val)
|
|
429
|
+
return false;
|
|
430
|
+
args.push({ name: nameTok.value, value: val });
|
|
431
|
+
return true;
|
|
432
|
+
};
|
|
433
|
+
if (!parsePair())
|
|
434
|
+
return null;
|
|
435
|
+
while (this.consume("punct", ",")) {
|
|
436
|
+
if (!parsePair())
|
|
437
|
+
break;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const close = this.expect("punct", ")");
|
|
441
|
+
return { kind: "call-named", callee, args, start, end: close?.end ?? start };
|
|
442
|
+
}
|
|
443
|
+
parseIf() {
|
|
444
|
+
const start = this.peek()?.start ?? 0;
|
|
445
|
+
this.advance(); // consume "if"
|
|
446
|
+
const condition = this.parseExpression(0);
|
|
447
|
+
if (!condition)
|
|
448
|
+
return null;
|
|
449
|
+
if (!this.expect("keyword", "then"))
|
|
450
|
+
return null;
|
|
451
|
+
const then = this.parseExpression(0);
|
|
452
|
+
if (!then)
|
|
453
|
+
return null;
|
|
454
|
+
if (!this.expect("keyword", "else"))
|
|
455
|
+
return null;
|
|
456
|
+
const els = this.parseExpression(0);
|
|
457
|
+
if (!els)
|
|
458
|
+
return null;
|
|
459
|
+
return { kind: "if", condition, then, else: els, start, end: els.end };
|
|
460
|
+
}
|
|
461
|
+
parseFor() {
|
|
462
|
+
const start = this.peek()?.start ?? 0;
|
|
463
|
+
this.advance(); // consume "for"
|
|
464
|
+
const bindings = [];
|
|
465
|
+
const parseBinding = () => {
|
|
466
|
+
const nameTok = this.advance();
|
|
467
|
+
if (!nameTok || (nameTok.kind !== "name" && nameTok.kind !== "backtick"))
|
|
468
|
+
return false;
|
|
469
|
+
const varName = nameTok.value;
|
|
470
|
+
if (!this.expect("keyword", "in"))
|
|
471
|
+
return false;
|
|
472
|
+
const domain = this.parseExpression(0);
|
|
473
|
+
if (!domain)
|
|
474
|
+
return false;
|
|
475
|
+
bindings.push({ name: varName, domain });
|
|
476
|
+
return true;
|
|
477
|
+
};
|
|
478
|
+
if (!parseBinding())
|
|
479
|
+
return null;
|
|
480
|
+
while (this.consume("punct", ",")) {
|
|
481
|
+
if (!parseBinding())
|
|
482
|
+
break;
|
|
483
|
+
}
|
|
484
|
+
if (!this.expect("keyword", "return"))
|
|
485
|
+
return null;
|
|
486
|
+
const body = this.parseExpression(0);
|
|
487
|
+
if (!body)
|
|
488
|
+
return null;
|
|
489
|
+
return { kind: "for", bindings, body, start, end: body.end };
|
|
490
|
+
}
|
|
491
|
+
parseQuantifier(kind) {
|
|
492
|
+
const start = this.peek()?.start ?? 0;
|
|
493
|
+
this.advance(); // consume "some" or "every"
|
|
494
|
+
const bindings = [];
|
|
495
|
+
const parseBinding = () => {
|
|
496
|
+
const nameTok = this.advance();
|
|
497
|
+
if (!nameTok || (nameTok.kind !== "name" && nameTok.kind !== "backtick"))
|
|
498
|
+
return false;
|
|
499
|
+
const varName = nameTok.value;
|
|
500
|
+
if (!this.expect("keyword", "in"))
|
|
501
|
+
return false;
|
|
502
|
+
const domain = this.parseExpression(0);
|
|
503
|
+
if (!domain)
|
|
504
|
+
return false;
|
|
505
|
+
bindings.push({ name: varName, domain });
|
|
506
|
+
return true;
|
|
507
|
+
};
|
|
508
|
+
if (!parseBinding())
|
|
509
|
+
return null;
|
|
510
|
+
while (this.consume("punct", ",")) {
|
|
511
|
+
if (!parseBinding())
|
|
512
|
+
break;
|
|
513
|
+
}
|
|
514
|
+
if (!this.expect("keyword", "satisfies"))
|
|
515
|
+
return null;
|
|
516
|
+
const satisfies = this.parseExpression(0);
|
|
517
|
+
if (!satisfies)
|
|
518
|
+
return null;
|
|
519
|
+
return { kind, bindings, satisfies, start, end: satisfies.end };
|
|
520
|
+
}
|
|
521
|
+
parseFunctionDef() {
|
|
522
|
+
const start = this.peek()?.start ?? 0;
|
|
523
|
+
this.advance(); // consume "function"
|
|
524
|
+
if (!this.expect("punct", "("))
|
|
525
|
+
return null;
|
|
526
|
+
const params = [];
|
|
527
|
+
if (!this.check("punct", ")")) {
|
|
528
|
+
const p = this.advance();
|
|
529
|
+
if (p)
|
|
530
|
+
params.push(p.value);
|
|
531
|
+
while (this.consume("punct", ",")) {
|
|
532
|
+
const q = this.advance();
|
|
533
|
+
if (q)
|
|
534
|
+
params.push(q.value);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (!this.expect("punct", ")"))
|
|
538
|
+
return null;
|
|
539
|
+
const body = this.parseExpression(0);
|
|
540
|
+
if (!body)
|
|
541
|
+
return null;
|
|
542
|
+
return { kind: "function-def", params, body, start, end: body.end };
|
|
543
|
+
}
|
|
544
|
+
parseNot() {
|
|
545
|
+
const start = this.peek()?.start ?? 0;
|
|
546
|
+
this.advance(); // consume "not"
|
|
547
|
+
if (!this.check("punct", "(")) {
|
|
548
|
+
// not as prefix operator for boolean: not expression
|
|
549
|
+
const operand = this.parseExpression(70);
|
|
550
|
+
if (!operand)
|
|
551
|
+
return null;
|
|
552
|
+
return {
|
|
553
|
+
kind: "call",
|
|
554
|
+
callee: "not",
|
|
555
|
+
args: [operand],
|
|
556
|
+
start,
|
|
557
|
+
end: operand.end,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
this.advance(); // consume (
|
|
561
|
+
const expr = this.parseExpression(0);
|
|
562
|
+
if (!expr)
|
|
563
|
+
return null;
|
|
564
|
+
const close = this.expect("punct", ")");
|
|
565
|
+
return { kind: "call", callee: "not", args: [expr], start, end: close?.end ?? expr.end };
|
|
566
|
+
}
|
|
567
|
+
parseParenOrRange() {
|
|
568
|
+
const open = this.peek();
|
|
569
|
+
if (!open)
|
|
570
|
+
return null;
|
|
571
|
+
const start = open.start;
|
|
572
|
+
this.advance(); // consume (
|
|
573
|
+
const expr = this.parseExpression(0);
|
|
574
|
+
if (!expr)
|
|
575
|
+
return null;
|
|
576
|
+
// Range: (a..b]
|
|
577
|
+
if (this.check("op", "..")) {
|
|
578
|
+
this.advance();
|
|
579
|
+
const high = this.parseExpression(0);
|
|
580
|
+
if (!high)
|
|
581
|
+
return null;
|
|
582
|
+
const close = this.advance();
|
|
583
|
+
const endIncluded = close?.value === "]";
|
|
584
|
+
return {
|
|
585
|
+
kind: "range",
|
|
586
|
+
startIncluded: false,
|
|
587
|
+
low: expr,
|
|
588
|
+
high,
|
|
589
|
+
endIncluded,
|
|
590
|
+
start,
|
|
591
|
+
end: close?.end ?? high.end,
|
|
592
|
+
};
|
|
593
|
+
}
|
|
594
|
+
this.expect("punct", ")");
|
|
595
|
+
return expr;
|
|
596
|
+
}
|
|
597
|
+
parseListOrRange() {
|
|
598
|
+
const open = this.peek();
|
|
599
|
+
if (!open)
|
|
600
|
+
return null;
|
|
601
|
+
const start = open.start;
|
|
602
|
+
this.advance(); // consume [
|
|
603
|
+
// Empty list
|
|
604
|
+
if (this.check("punct", "]")) {
|
|
605
|
+
this.advance();
|
|
606
|
+
return { kind: "list", items: [], start, end: open.end + 1 };
|
|
607
|
+
}
|
|
608
|
+
const first = this.parseExpression(0);
|
|
609
|
+
if (!first)
|
|
610
|
+
return null;
|
|
611
|
+
// Range: [a..b) or [a..b]
|
|
612
|
+
if (this.check("op", "..")) {
|
|
613
|
+
this.advance();
|
|
614
|
+
const high = this.parseExpression(0);
|
|
615
|
+
if (!high)
|
|
616
|
+
return null;
|
|
617
|
+
const close = this.advance();
|
|
618
|
+
const endIncluded = close?.value === "]";
|
|
619
|
+
return {
|
|
620
|
+
kind: "range",
|
|
621
|
+
startIncluded: true,
|
|
622
|
+
low: first,
|
|
623
|
+
high,
|
|
624
|
+
endIncluded,
|
|
625
|
+
start,
|
|
626
|
+
end: close?.end ?? high.end,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
// List
|
|
630
|
+
const items = [first];
|
|
631
|
+
while (this.consume("punct", ",")) {
|
|
632
|
+
const item = this.parseExpression(0);
|
|
633
|
+
if (item)
|
|
634
|
+
items.push(item);
|
|
635
|
+
}
|
|
636
|
+
const close = this.expect("punct", "]");
|
|
637
|
+
return { kind: "list", items, start, end: close?.end ?? first.end };
|
|
638
|
+
}
|
|
639
|
+
parseContext() {
|
|
640
|
+
const open = this.peek();
|
|
641
|
+
if (!open)
|
|
642
|
+
return null;
|
|
643
|
+
const start = open.start;
|
|
644
|
+
this.advance(); // consume {
|
|
645
|
+
const entries = [];
|
|
646
|
+
if (!this.check("punct", "}")) {
|
|
647
|
+
const parseEntry = () => {
|
|
648
|
+
// Key: string literal or name
|
|
649
|
+
let key;
|
|
650
|
+
if (this.check("string")) {
|
|
651
|
+
const t = this.advance();
|
|
652
|
+
if (!t)
|
|
653
|
+
return false;
|
|
654
|
+
key = t.value.slice(1, -1);
|
|
655
|
+
}
|
|
656
|
+
else if (this.check("name") || this.check("keyword")) {
|
|
657
|
+
const t = this.advance();
|
|
658
|
+
if (!t)
|
|
659
|
+
return false;
|
|
660
|
+
key = t.value;
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
const t = this.peek();
|
|
664
|
+
this.errors.push({
|
|
665
|
+
message: "Expected context key",
|
|
666
|
+
start: t?.start ?? 0,
|
|
667
|
+
end: t?.end ?? 0,
|
|
668
|
+
});
|
|
669
|
+
return false;
|
|
670
|
+
}
|
|
671
|
+
if (!this.expect("punct", ":"))
|
|
672
|
+
return false;
|
|
673
|
+
const val = this.parseExpression(0);
|
|
674
|
+
if (!val)
|
|
675
|
+
return false;
|
|
676
|
+
entries.push({ key, value: val });
|
|
677
|
+
return true;
|
|
678
|
+
};
|
|
679
|
+
if (!parseEntry()) {
|
|
680
|
+
// error recovery: skip to }
|
|
681
|
+
while (this.peek() && !this.check("punct", "}"))
|
|
682
|
+
this.advance();
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
while (this.consume("punct", ",")) {
|
|
686
|
+
if (!parseEntry())
|
|
687
|
+
break;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
const close = this.expect("punct", "}");
|
|
692
|
+
return { kind: "context", entries, start, end: close?.end ?? start };
|
|
693
|
+
}
|
|
694
|
+
parseInTestExpr() {
|
|
695
|
+
// x in (a, b, c) or x in [1..5] or x in expr
|
|
696
|
+
if (this.check("punct", "(")) {
|
|
697
|
+
// Parenthesized list of tests or a range
|
|
698
|
+
return this.parseParenOrRange();
|
|
699
|
+
}
|
|
700
|
+
if (this.check("punct", "[")) {
|
|
701
|
+
return this.parseListOrRange();
|
|
702
|
+
}
|
|
703
|
+
return this.parseExpression(30);
|
|
704
|
+
}
|
|
705
|
+
parseTypeName() {
|
|
706
|
+
const tok = this.peek();
|
|
707
|
+
if (!tok || (tok.kind !== "name" && tok.kind !== "keyword"))
|
|
708
|
+
return null;
|
|
709
|
+
this.advance();
|
|
710
|
+
const name = tok.value;
|
|
711
|
+
// Handle multi-word type names: "date and time", "years and months duration", etc.
|
|
712
|
+
const multiTypes = {
|
|
713
|
+
date: "date",
|
|
714
|
+
time: "time",
|
|
715
|
+
number: "number",
|
|
716
|
+
string: "string",
|
|
717
|
+
boolean: "boolean",
|
|
718
|
+
context: "context",
|
|
719
|
+
list: "list",
|
|
720
|
+
function: "function",
|
|
721
|
+
duration: "duration",
|
|
722
|
+
Any: "Any",
|
|
723
|
+
};
|
|
724
|
+
if (multiTypes[name])
|
|
725
|
+
return name;
|
|
726
|
+
// Try to extend: "date and time", "years and months duration"
|
|
727
|
+
const next1 = this.peek();
|
|
728
|
+
if (next1?.kind === "keyword" && next1.value === "and") {
|
|
729
|
+
const saved = this.pos;
|
|
730
|
+
this.advance(); // consume "and"
|
|
731
|
+
const next2 = this.peek();
|
|
732
|
+
if (next2?.kind === "name" && next2.value === "time") {
|
|
733
|
+
this.advance();
|
|
734
|
+
return "date and time";
|
|
735
|
+
}
|
|
736
|
+
if (next2?.kind === "name" && next2.value === "months") {
|
|
737
|
+
this.advance();
|
|
738
|
+
const next3 = this.peek();
|
|
739
|
+
if (next3?.kind === "name" && next3.value === "duration") {
|
|
740
|
+
this.advance();
|
|
741
|
+
return "years and months duration";
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
this.pos = saved;
|
|
745
|
+
}
|
|
746
|
+
return name;
|
|
747
|
+
}
|
|
748
|
+
// -------------------------------------------------------------------------
|
|
749
|
+
// Unary-test mode
|
|
750
|
+
// -------------------------------------------------------------------------
|
|
751
|
+
parseUnaryTests() {
|
|
752
|
+
const start = this.peek()?.start ?? 0;
|
|
753
|
+
// "-" means any input
|
|
754
|
+
if (this.check("op", "-") && this.tokens.length === 1) {
|
|
755
|
+
this.advance();
|
|
756
|
+
const tok = this.tokens[0];
|
|
757
|
+
return { kind: "any-input", start: tok?.start ?? 0, end: tok?.end ?? 1 };
|
|
758
|
+
}
|
|
759
|
+
const tests = [];
|
|
760
|
+
const test = this.parseOneUnaryTest();
|
|
761
|
+
if (!test)
|
|
762
|
+
return null;
|
|
763
|
+
tests.push(test);
|
|
764
|
+
while (this.consume("punct", ",")) {
|
|
765
|
+
const t = this.parseOneUnaryTest();
|
|
766
|
+
if (t)
|
|
767
|
+
tests.push(t);
|
|
768
|
+
}
|
|
769
|
+
if (tests.length === 1) {
|
|
770
|
+
const t = tests[0];
|
|
771
|
+
if (t)
|
|
772
|
+
return t;
|
|
773
|
+
}
|
|
774
|
+
const last = tests[tests.length - 1];
|
|
775
|
+
return { kind: "unary-test-list", tests, start, end: last?.end ?? start };
|
|
776
|
+
}
|
|
777
|
+
parseOneUnaryTest() {
|
|
778
|
+
const tok = this.peek();
|
|
779
|
+
if (!tok)
|
|
780
|
+
return null;
|
|
781
|
+
// "-" = any input
|
|
782
|
+
if (tok.kind === "op" && tok.value === "-") {
|
|
783
|
+
this.advance();
|
|
784
|
+
return { kind: "any-input", start: tok.start, end: tok.end };
|
|
785
|
+
}
|
|
786
|
+
// not(...) wrapping
|
|
787
|
+
if (tok.kind === "keyword" && tok.value === "not") {
|
|
788
|
+
const start = tok.start;
|
|
789
|
+
this.advance();
|
|
790
|
+
if (!this.expect("punct", "("))
|
|
791
|
+
return null;
|
|
792
|
+
const inner = this.parseUnaryTestsInner();
|
|
793
|
+
const close = this.expect("punct", ")");
|
|
794
|
+
return { kind: "unary-not", tests: inner, start, end: close?.end ?? start };
|
|
795
|
+
}
|
|
796
|
+
// Comparison operator prefix: < 5, >= 10, etc.
|
|
797
|
+
if (tok.kind === "op" &&
|
|
798
|
+
(tok.value === "<" || tok.value === "<=" || tok.value === ">" || tok.value === ">=")) {
|
|
799
|
+
const start = tok.start;
|
|
800
|
+
this.advance();
|
|
801
|
+
const expr = this.parseExpression(0);
|
|
802
|
+
if (!expr)
|
|
803
|
+
return null;
|
|
804
|
+
const op = tok.value;
|
|
805
|
+
const input = { kind: "name", name: "?", start: tok.start, end: tok.start };
|
|
806
|
+
return { kind: "binary", op, left: input, right: expr, start, end: expr.end };
|
|
807
|
+
}
|
|
808
|
+
// Ranges
|
|
809
|
+
if (tok.kind === "punct" && (tok.value === "[" || tok.value === "(")) {
|
|
810
|
+
return this.parseListOrRange() ?? this.parseParenOrRange();
|
|
811
|
+
}
|
|
812
|
+
// Expression equality test
|
|
813
|
+
return this.parseExpression(0);
|
|
814
|
+
}
|
|
815
|
+
parseUnaryTestsInner() {
|
|
816
|
+
const tests = [];
|
|
817
|
+
const t = this.parseOneUnaryTest();
|
|
818
|
+
if (t)
|
|
819
|
+
tests.push(t);
|
|
820
|
+
while (this.consume("punct", ",")) {
|
|
821
|
+
const next = this.parseOneUnaryTest();
|
|
822
|
+
if (next)
|
|
823
|
+
tests.push(next);
|
|
824
|
+
}
|
|
825
|
+
return tests;
|
|
826
|
+
}
|
|
827
|
+
/** Push an error if there are unconsumed tokens remaining. */
|
|
828
|
+
checkDone() {
|
|
829
|
+
const tok = this.tokens[this.pos];
|
|
830
|
+
if (tok) {
|
|
831
|
+
this.errors.push({
|
|
832
|
+
message: `Unexpected token '${tok.value}'`,
|
|
833
|
+
start: tok.start,
|
|
834
|
+
end: tok.end,
|
|
835
|
+
});
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
export function parseExpression(input) {
|
|
840
|
+
const p = new Parser(input);
|
|
841
|
+
const ast = p.parseExpression(0);
|
|
842
|
+
p.checkDone();
|
|
843
|
+
return { ast, errors: p.errors };
|
|
844
|
+
}
|
|
845
|
+
export function parseUnaryTests(input) {
|
|
846
|
+
if (input.trim() === "-") {
|
|
847
|
+
return { ast: { kind: "any-input", start: 0, end: input.length }, errors: [] };
|
|
848
|
+
}
|
|
849
|
+
const p = new Parser(input);
|
|
850
|
+
const ast = p.parseUnaryTests();
|
|
851
|
+
return { ast, errors: p.errors };
|
|
852
|
+
}
|
|
853
|
+
//# sourceMappingURL=parser.js.map
|