@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/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