@bpmnkit/feel 0.0.21 → 0.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/README.md +1 -0
- package/dist/ast.d.ts +4 -0
- package/dist/builtins.d.ts +7 -0
- package/dist/builtins.js +778 -164
- package/dist/evaluator.d.ts +4 -1
- package/dist/evaluator.js +251 -139
- package/dist/formatter.js +16 -1
- package/dist/index.d.ts +1 -1
- package/dist/lexer.d.ts +7 -0
- package/dist/lexer.js +92 -5
- package/dist/parser.d.ts +11 -2
- package/dist/parser.js +315 -98
- package/dist/types.js +35 -2
- package/package.json +5 -2
package/dist/parser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { tokenize } from "./lexer.js";
|
|
1
|
+
import { tokenize, unescapeString } from "./lexer.js";
|
|
2
2
|
// All known multi-word built-in names
|
|
3
3
|
const BUILTIN_NAMES = new Set([
|
|
4
4
|
"string length",
|
|
@@ -48,12 +48,43 @@ const BUILTIN_PREFIXES = (() => {
|
|
|
48
48
|
}
|
|
49
49
|
return s;
|
|
50
50
|
})();
|
|
51
|
+
/** Every strict prefix of the given multi-word names. */
|
|
52
|
+
function prefixesOf(names) {
|
|
53
|
+
const prefixes = new Set();
|
|
54
|
+
for (const name of names) {
|
|
55
|
+
if (!name.includes(" "))
|
|
56
|
+
continue;
|
|
57
|
+
const parts = name.split(" ");
|
|
58
|
+
for (let i = 1; i < parts.length; i++)
|
|
59
|
+
prefixes.add(parts.slice(0, i).join(" "));
|
|
60
|
+
}
|
|
61
|
+
return prefixes;
|
|
62
|
+
}
|
|
63
|
+
// The property names FEEL spells with a space. A path key is matched against
|
|
64
|
+
// this fixed set rather than joining words greedily, so `a.b and c` still
|
|
65
|
+
// reads as a conjunction.
|
|
66
|
+
const MULTIWORD_PROPERTIES = ["time offset", "start included", "end included"];
|
|
67
|
+
// The symbols a FEEL name may contain besides letters, digits and spaces.
|
|
68
|
+
const NAME_SYMBOLS = new Set([".", "/", "-", "'", "+", "*"]);
|
|
69
|
+
// Multi-word type names, longest first: a prefix must not win over the whole name.
|
|
70
|
+
const MULTIWORD_TYPES = ["years and months duration", "days and time duration", "date and time"];
|
|
51
71
|
class Parser {
|
|
52
72
|
tokens;
|
|
53
73
|
pos = 0;
|
|
74
|
+
names;
|
|
75
|
+
namePrefixes;
|
|
54
76
|
errors = [];
|
|
55
|
-
constructor(input) {
|
|
77
|
+
constructor(input, options = {}) {
|
|
56
78
|
this.tokens = tokenize(input).filter((t) => t.kind !== "whitespace" && t.kind !== "comment");
|
|
79
|
+
const scopeNames = options.names ? [...options.names].filter((n) => n.includes(" ")) : [];
|
|
80
|
+
this.names = new Set(scopeNames);
|
|
81
|
+
this.namePrefixes = prefixesOf(scopeNames);
|
|
82
|
+
}
|
|
83
|
+
isKnownName(name) {
|
|
84
|
+
return BUILTIN_NAMES.has(name) || this.names.has(name);
|
|
85
|
+
}
|
|
86
|
+
isKnownPrefix(name) {
|
|
87
|
+
return BUILTIN_PREFIXES.has(name) || this.namePrefixes.has(name);
|
|
57
88
|
}
|
|
58
89
|
peek(offset = 0) {
|
|
59
90
|
return this.tokens[this.pos + offset];
|
|
@@ -85,26 +116,32 @@ class Parser {
|
|
|
85
116
|
}
|
|
86
117
|
return tok;
|
|
87
118
|
}
|
|
88
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Extends a single name token into the longest multi-word name in scope,
|
|
121
|
+
* built-in or supplied by the caller. Words consumed while reaching for a
|
|
122
|
+
* longer name that does not exist are given back, so `date and` falls back
|
|
123
|
+
* to `date` rather than becoming a name nothing can resolve.
|
|
124
|
+
*/
|
|
89
125
|
resolveMultiwordName(first) {
|
|
90
126
|
let name = first;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
break;
|
|
127
|
+
let longest = first;
|
|
128
|
+
let longestPos = this.pos;
|
|
129
|
+
while (this.isKnownPrefix(name)) {
|
|
95
130
|
const next = this.peek();
|
|
96
131
|
if (!next || (next.kind !== "name" && next.kind !== "keyword"))
|
|
97
132
|
break;
|
|
98
133
|
const extended = `${name} ${next.value}`;
|
|
99
|
-
if (
|
|
100
|
-
this.advance();
|
|
101
|
-
name = extended;
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
134
|
+
if (!this.isKnownName(extended) && !this.isKnownPrefix(extended))
|
|
104
135
|
break;
|
|
136
|
+
this.advance();
|
|
137
|
+
name = extended;
|
|
138
|
+
if (this.isKnownName(name)) {
|
|
139
|
+
longest = name;
|
|
140
|
+
longestPos = this.pos;
|
|
105
141
|
}
|
|
106
142
|
}
|
|
107
|
-
|
|
143
|
+
this.pos = longestPos;
|
|
144
|
+
return longest;
|
|
108
145
|
}
|
|
109
146
|
// -------------------------------------------------------------------------
|
|
110
147
|
// Expression parsing (Pratt)
|
|
@@ -160,6 +197,8 @@ class Parser {
|
|
|
160
197
|
return 80;
|
|
161
198
|
if (tok.kind === "punct" && tok.value === "[")
|
|
162
199
|
return 80;
|
|
200
|
+
if (tok.kind === "punct" && tok.value === "(")
|
|
201
|
+
return 80;
|
|
163
202
|
return 0;
|
|
164
203
|
}
|
|
165
204
|
parseInfix(left, prec) {
|
|
@@ -199,9 +238,9 @@ class Parser {
|
|
|
199
238
|
tok.value === "**")) {
|
|
200
239
|
this.advance();
|
|
201
240
|
const op = tok.value;
|
|
202
|
-
//
|
|
203
|
-
|
|
204
|
-
const right = this.parseExpression(
|
|
241
|
+
// FEEL makes every infix operator left-associative, "**" included:
|
|
242
|
+
// 2 ** 3 ** 2 is (2 ** 3) ** 2.
|
|
243
|
+
const right = this.parseExpression(prec);
|
|
205
244
|
if (!right) {
|
|
206
245
|
const pos = tok.end;
|
|
207
246
|
this.errors.push({
|
|
@@ -258,8 +297,40 @@ class Parser {
|
|
|
258
297
|
this.errors.push({ message: "Expected name after '.'", start: tok.start, end: tok.end });
|
|
259
298
|
return null;
|
|
260
299
|
}
|
|
300
|
+
let key = nameTok.value;
|
|
301
|
+
let end = nameTok.end;
|
|
302
|
+
const multiword = MULTIWORD_PROPERTIES.find((p) => this.tryConsumeWords(p));
|
|
303
|
+
if (multiword) {
|
|
304
|
+
key = multiword;
|
|
305
|
+
end = this.tokens[this.pos - 1]?.end ?? nameTok.end;
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
this.advance();
|
|
309
|
+
}
|
|
310
|
+
return { kind: "path", base: left, key, start: left.start, end };
|
|
311
|
+
}
|
|
312
|
+
// Invocation of a function-valued expression: expr(args)
|
|
313
|
+
if (tok.kind === "punct" && tok.value === "(") {
|
|
261
314
|
this.advance();
|
|
262
|
-
|
|
315
|
+
const args = [];
|
|
316
|
+
if (!this.check("punct", ")")) {
|
|
317
|
+
const arg = this.parseExpression(0);
|
|
318
|
+
if (arg)
|
|
319
|
+
args.push(arg);
|
|
320
|
+
while (this.consume("punct", ",")) {
|
|
321
|
+
const a = this.parseExpression(0);
|
|
322
|
+
if (a)
|
|
323
|
+
args.push(a);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const close = this.expect("punct", ")");
|
|
327
|
+
return {
|
|
328
|
+
kind: "call-expr",
|
|
329
|
+
target: left,
|
|
330
|
+
args,
|
|
331
|
+
start: left.start,
|
|
332
|
+
end: close?.end ?? args[args.length - 1]?.end ?? left.end,
|
|
333
|
+
};
|
|
263
334
|
}
|
|
264
335
|
// Filter: expr[condition]
|
|
265
336
|
if (tok.kind === "punct" && tok.value === "[") {
|
|
@@ -298,7 +369,7 @@ class Parser {
|
|
|
298
369
|
// String literal
|
|
299
370
|
if (tok.kind === "string") {
|
|
300
371
|
this.advance();
|
|
301
|
-
const raw = tok.value.slice(1, -1)
|
|
372
|
+
const raw = unescapeString(tok.value.slice(1, -1));
|
|
302
373
|
return { kind: "string", value: raw, start: tok.start, end: tok.end };
|
|
303
374
|
}
|
|
304
375
|
// Temporal literal
|
|
@@ -406,28 +477,45 @@ class Parser {
|
|
|
406
477
|
return { kind: "name", name, start, end: this.tokens[this.pos - 1]?.end ?? start };
|
|
407
478
|
}
|
|
408
479
|
isNamedArgList() {
|
|
409
|
-
// Peek: name colon
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
480
|
+
// Peek: one or more name words followed by a colon? Parameter names may
|
|
481
|
+
// have spaces, as in `substring(start position: 2, string: "hello")`.
|
|
482
|
+
let offset = 0;
|
|
483
|
+
while (true) {
|
|
484
|
+
const tok = this.peek(offset);
|
|
485
|
+
if (!tok || (tok.kind !== "name" && tok.kind !== "keyword"))
|
|
486
|
+
break;
|
|
487
|
+
offset++;
|
|
488
|
+
}
|
|
489
|
+
if (offset === 0)
|
|
490
|
+
return false;
|
|
491
|
+
const after = this.peek(offset);
|
|
492
|
+
return after?.kind === "punct" && after.value === ":";
|
|
417
493
|
}
|
|
418
494
|
parseNamedCall(callee, start) {
|
|
419
495
|
const args = [];
|
|
420
496
|
if (!this.check("punct", ")")) {
|
|
421
497
|
const parsePair = () => {
|
|
422
|
-
const
|
|
423
|
-
|
|
498
|
+
const words = [];
|
|
499
|
+
while (this.check("name") || this.check("keyword")) {
|
|
500
|
+
const word = this.advance();
|
|
501
|
+
if (word)
|
|
502
|
+
words.push(word.value);
|
|
503
|
+
}
|
|
504
|
+
if (words.length === 0) {
|
|
505
|
+
const t = this.peek();
|
|
506
|
+
this.errors.push({
|
|
507
|
+
message: "Expected parameter name",
|
|
508
|
+
start: t?.start ?? 0,
|
|
509
|
+
end: t?.end ?? 0,
|
|
510
|
+
});
|
|
424
511
|
return false;
|
|
512
|
+
}
|
|
425
513
|
if (!this.expect("punct", ":"))
|
|
426
514
|
return false;
|
|
427
515
|
const val = this.parseExpression(0);
|
|
428
516
|
if (!val)
|
|
429
517
|
return false;
|
|
430
|
-
args.push({ name:
|
|
518
|
+
args.push({ name: words.join(" "), value: val });
|
|
431
519
|
return true;
|
|
432
520
|
};
|
|
433
521
|
if (!parsePair())
|
|
@@ -440,6 +528,56 @@ class Parser {
|
|
|
440
528
|
const close = this.expect("punct", ")");
|
|
441
529
|
return { kind: "call-named", callee, args, start, end: close?.end ?? start };
|
|
442
530
|
}
|
|
531
|
+
/**
|
|
532
|
+
* Parses the domain of a `for`/`some`/`every` binding. Unlike other
|
|
533
|
+
* positions, a range may appear here undelimited: `for i in 1..3`.
|
|
534
|
+
*/
|
|
535
|
+
parseIterationDomain() {
|
|
536
|
+
const domain = this.parseExpression(0);
|
|
537
|
+
if (!domain)
|
|
538
|
+
return null;
|
|
539
|
+
if (!this.check("op", ".."))
|
|
540
|
+
return domain;
|
|
541
|
+
this.advance();
|
|
542
|
+
const high = this.parseExpression(0);
|
|
543
|
+
if (!high)
|
|
544
|
+
return null;
|
|
545
|
+
return {
|
|
546
|
+
kind: "range",
|
|
547
|
+
startIncluded: true,
|
|
548
|
+
low: domain,
|
|
549
|
+
high,
|
|
550
|
+
endIncluded: true,
|
|
551
|
+
start: domain.start,
|
|
552
|
+
end: high.end,
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Reads a context key, which runs up to the colon and so can be gathered
|
|
557
|
+
* without ambiguity. A FEEL name may hold spaces and the symbols listed in
|
|
558
|
+
* NAME_SYMBOLS, which is what lets `{_2021-01-11: ...}` and `{foo+bar: ...}`
|
|
559
|
+
* be keys rather than arithmetic.
|
|
560
|
+
*/
|
|
561
|
+
parseContextKey() {
|
|
562
|
+
let key = "";
|
|
563
|
+
let previousWasWord = false;
|
|
564
|
+
while (true) {
|
|
565
|
+
const tok = this.peek();
|
|
566
|
+
if (!tok)
|
|
567
|
+
break;
|
|
568
|
+
const isWord = tok.kind === "name" || tok.kind === "keyword" || tok.kind === "number";
|
|
569
|
+
const isSymbol = tok.kind === "op" && NAME_SYMBOLS.has(tok.value);
|
|
570
|
+
if (!isWord && !isSymbol)
|
|
571
|
+
break;
|
|
572
|
+
// Two words in a row are separated by the space that separated them.
|
|
573
|
+
if (isWord && previousWasWord)
|
|
574
|
+
key += " ";
|
|
575
|
+
key += tok.value;
|
|
576
|
+
previousWasWord = isWord;
|
|
577
|
+
this.advance();
|
|
578
|
+
}
|
|
579
|
+
return key;
|
|
580
|
+
}
|
|
443
581
|
parseIf() {
|
|
444
582
|
const start = this.peek()?.start ?? 0;
|
|
445
583
|
this.advance(); // consume "if"
|
|
@@ -469,7 +607,7 @@ class Parser {
|
|
|
469
607
|
const varName = nameTok.value;
|
|
470
608
|
if (!this.expect("keyword", "in"))
|
|
471
609
|
return false;
|
|
472
|
-
const domain = this.
|
|
610
|
+
const domain = this.parseIterationDomain();
|
|
473
611
|
if (!domain)
|
|
474
612
|
return false;
|
|
475
613
|
bindings.push({ name: varName, domain });
|
|
@@ -499,7 +637,7 @@ class Parser {
|
|
|
499
637
|
const varName = nameTok.value;
|
|
500
638
|
if (!this.expect("keyword", "in"))
|
|
501
639
|
return false;
|
|
502
|
-
const domain = this.
|
|
640
|
+
const domain = this.parseIterationDomain();
|
|
503
641
|
if (!domain)
|
|
504
642
|
return false;
|
|
505
643
|
bindings.push({ name: varName, domain });
|
|
@@ -524,15 +662,19 @@ class Parser {
|
|
|
524
662
|
if (!this.expect("punct", "("))
|
|
525
663
|
return null;
|
|
526
664
|
const params = [];
|
|
665
|
+
// A parameter may declare a type, which this package does not check:
|
|
666
|
+
// `function(a: number) a + 1`.
|
|
667
|
+
const parseParam = () => {
|
|
668
|
+
const name = this.advance();
|
|
669
|
+
if (name)
|
|
670
|
+
params.push(name.value);
|
|
671
|
+
if (this.consume("punct", ":"))
|
|
672
|
+
this.parseTypeName();
|
|
673
|
+
};
|
|
527
674
|
if (!this.check("punct", ")")) {
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
while (this.consume("punct", ",")) {
|
|
532
|
-
const q = this.advance();
|
|
533
|
-
if (q)
|
|
534
|
-
params.push(q.value);
|
|
535
|
-
}
|
|
675
|
+
parseParam();
|
|
676
|
+
while (this.consume("punct", ","))
|
|
677
|
+
parseParam();
|
|
536
678
|
}
|
|
537
679
|
if (!this.expect("punct", ")"))
|
|
538
680
|
return null;
|
|
@@ -651,13 +793,10 @@ class Parser {
|
|
|
651
793
|
const t = this.advance();
|
|
652
794
|
if (!t)
|
|
653
795
|
return false;
|
|
654
|
-
key = t.value.slice(1, -1);
|
|
796
|
+
key = unescapeString(t.value.slice(1, -1));
|
|
655
797
|
}
|
|
656
798
|
else if (this.check("name") || this.check("keyword")) {
|
|
657
|
-
|
|
658
|
-
if (!t)
|
|
659
|
-
return false;
|
|
660
|
-
key = t.value;
|
|
799
|
+
key = this.parseContextKey();
|
|
661
800
|
}
|
|
662
801
|
else {
|
|
663
802
|
const t = this.peek();
|
|
@@ -691,59 +830,119 @@ class Parser {
|
|
|
691
830
|
const close = this.expect("punct", "}");
|
|
692
831
|
return { kind: "context", entries, start, end: close?.end ?? start };
|
|
693
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* Parses the right-hand side of `in`, which FEEL defines as a positive
|
|
835
|
+
* unary test rather than an expression: `x in <= 10`, `x in [1..5]`,
|
|
836
|
+
* `x in (1, < 5, >= 10)`, `x in y`.
|
|
837
|
+
*/
|
|
694
838
|
parseInTestExpr() {
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
839
|
+
if (this.check("punct", "("))
|
|
840
|
+
return this.parseInParen();
|
|
841
|
+
return this.parseOneUnaryTest();
|
|
842
|
+
}
|
|
843
|
+
/**
|
|
844
|
+
* A parenthesized `in` operand is a range, a comma-separated list of unary
|
|
845
|
+
* tests, or a plain grouped expression.
|
|
846
|
+
*/
|
|
847
|
+
parseInParen() {
|
|
848
|
+
const open = this.peek();
|
|
849
|
+
if (!open)
|
|
850
|
+
return null;
|
|
851
|
+
const start = open.start;
|
|
852
|
+
this.advance(); // consume (
|
|
853
|
+
const first = this.parseOneUnaryTest();
|
|
854
|
+
if (!first)
|
|
855
|
+
return null;
|
|
856
|
+
if (this.check("op", "..")) {
|
|
857
|
+
this.advance();
|
|
858
|
+
const high = this.parseExpression(0);
|
|
859
|
+
if (!high)
|
|
860
|
+
return null;
|
|
861
|
+
const close = this.advance();
|
|
862
|
+
return {
|
|
863
|
+
kind: "range",
|
|
864
|
+
startIncluded: false,
|
|
865
|
+
low: first,
|
|
866
|
+
high,
|
|
867
|
+
endIncluded: close?.value === "]",
|
|
868
|
+
start,
|
|
869
|
+
end: close?.end ?? high.end,
|
|
870
|
+
};
|
|
699
871
|
}
|
|
700
|
-
if (this.check("punct", "
|
|
701
|
-
|
|
872
|
+
if (this.check("punct", ",")) {
|
|
873
|
+
const tests = [first];
|
|
874
|
+
while (this.consume("punct", ",")) {
|
|
875
|
+
const test = this.parseOneUnaryTest();
|
|
876
|
+
if (test)
|
|
877
|
+
tests.push(test);
|
|
878
|
+
}
|
|
879
|
+
const close = this.expect("punct", ")");
|
|
880
|
+
return {
|
|
881
|
+
kind: "unary-test-list",
|
|
882
|
+
tests,
|
|
883
|
+
start,
|
|
884
|
+
end: close?.end ?? tests[tests.length - 1]?.end ?? start,
|
|
885
|
+
};
|
|
702
886
|
}
|
|
703
|
-
|
|
887
|
+
this.expect("punct", ")");
|
|
888
|
+
return first;
|
|
704
889
|
}
|
|
890
|
+
/**
|
|
891
|
+
* Parses a type name after `instance of`. Multi-word names are tried
|
|
892
|
+
* longest-first, since "date" is also the start of "date and time". Type
|
|
893
|
+
* arguments (`list<number>`, `function<number> -> string`) are consumed and
|
|
894
|
+
* ignored: this package checks the outer type only.
|
|
895
|
+
*/
|
|
705
896
|
parseTypeName() {
|
|
706
897
|
const tok = this.peek();
|
|
707
898
|
if (!tok || (tok.kind !== "name" && tok.kind !== "keyword"))
|
|
708
899
|
return null;
|
|
709
|
-
|
|
710
|
-
const
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
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";
|
|
900
|
+
let name = null;
|
|
901
|
+
for (const candidate of MULTIWORD_TYPES) {
|
|
902
|
+
if (this.tryConsumeWords(candidate)) {
|
|
903
|
+
name = candidate;
|
|
904
|
+
break;
|
|
735
905
|
}
|
|
736
|
-
|
|
906
|
+
}
|
|
907
|
+
if (name === null) {
|
|
908
|
+
this.advance();
|
|
909
|
+
name = tok.value;
|
|
910
|
+
}
|
|
911
|
+
this.skipTypeArguments();
|
|
912
|
+
return name;
|
|
913
|
+
}
|
|
914
|
+
/** Consumes the tokens spelling `phrase`, or nothing if they do not follow. */
|
|
915
|
+
tryConsumeWords(phrase) {
|
|
916
|
+
const words = phrase.split(" ");
|
|
917
|
+
for (let i = 0; i < words.length; i++) {
|
|
918
|
+
const tok = this.peek(i);
|
|
919
|
+
if (!tok || (tok.kind !== "name" && tok.kind !== "keyword") || tok.value !== words[i]) {
|
|
920
|
+
return false;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
this.pos += words.length;
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
/** Skips `<...>` type arguments and a `-> type` function result. */
|
|
927
|
+
skipTypeArguments() {
|
|
928
|
+
if (this.check("op", "<")) {
|
|
929
|
+
let depth = 0;
|
|
930
|
+
while (this.peek()) {
|
|
931
|
+
if (this.check("op", "<"))
|
|
932
|
+
depth++;
|
|
933
|
+
else if (this.check("op", ">"))
|
|
934
|
+
depth--;
|
|
935
|
+
else if (this.check("op", ">="))
|
|
936
|
+
depth--; // ">>" lexes as one token pair
|
|
737
937
|
this.advance();
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
this.advance();
|
|
741
|
-
return "years and months duration";
|
|
742
|
-
}
|
|
938
|
+
if (depth === 0)
|
|
939
|
+
break;
|
|
743
940
|
}
|
|
744
|
-
this.pos = saved;
|
|
745
941
|
}
|
|
746
|
-
|
|
942
|
+
if (this.check("op", "->")) {
|
|
943
|
+
this.advance();
|
|
944
|
+
this.parseTypeName();
|
|
945
|
+
}
|
|
747
946
|
}
|
|
748
947
|
// -------------------------------------------------------------------------
|
|
749
948
|
// Unary-test mode
|
|
@@ -793,9 +992,14 @@ class Parser {
|
|
|
793
992
|
const close = this.expect("punct", ")");
|
|
794
993
|
return { kind: "unary-not", tests: inner, start, end: close?.end ?? start };
|
|
795
994
|
}
|
|
796
|
-
// Comparison operator prefix: < 5, >= 10,
|
|
995
|
+
// Comparison operator prefix: < 5, >= 10, = 3, != 4
|
|
797
996
|
if (tok.kind === "op" &&
|
|
798
|
-
(tok.value === "<" ||
|
|
997
|
+
(tok.value === "<" ||
|
|
998
|
+
tok.value === "<=" ||
|
|
999
|
+
tok.value === ">" ||
|
|
1000
|
+
tok.value === ">=" ||
|
|
1001
|
+
tok.value === "=" ||
|
|
1002
|
+
tok.value === "!=")) {
|
|
799
1003
|
const start = tok.start;
|
|
800
1004
|
this.advance();
|
|
801
1005
|
const expr = this.parseExpression(0);
|
|
@@ -861,33 +1065,46 @@ class Parser {
|
|
|
861
1065
|
const PARSE_CACHE_LIMIT = 2048;
|
|
862
1066
|
const expressionCache = new Map();
|
|
863
1067
|
const unaryTestsCache = new Map();
|
|
864
|
-
function remember(cache,
|
|
1068
|
+
function remember(cache, key, result) {
|
|
865
1069
|
if (cache.size >= PARSE_CACHE_LIMIT)
|
|
866
1070
|
cache.clear();
|
|
867
|
-
cache.set(
|
|
1071
|
+
cache.set(key, result);
|
|
868
1072
|
return result;
|
|
869
1073
|
}
|
|
870
|
-
|
|
871
|
-
|
|
1074
|
+
/**
|
|
1075
|
+
* Cache key for a parse. Scope names change how an expression parses, so they
|
|
1076
|
+
* are part of the key; only names with spaces can, so the rest are left out to
|
|
1077
|
+
* keep the key small.
|
|
1078
|
+
*/
|
|
1079
|
+
function cacheKey(input, options) {
|
|
1080
|
+
if (!options?.names)
|
|
1081
|
+
return input;
|
|
1082
|
+
const relevant = [...options.names].filter((n) => n.includes(" ")).sort();
|
|
1083
|
+
return relevant.length === 0 ? input : `${input}\u0000${relevant.join("\u0001")}`;
|
|
1084
|
+
}
|
|
1085
|
+
export function parseExpression(input, options) {
|
|
1086
|
+
const key = cacheKey(input, options);
|
|
1087
|
+
const cached = expressionCache.get(key);
|
|
872
1088
|
if (cached !== undefined)
|
|
873
1089
|
return cached;
|
|
874
|
-
const p = new Parser(input);
|
|
1090
|
+
const p = new Parser(input, options);
|
|
875
1091
|
const ast = p.parseExpression(0);
|
|
876
1092
|
p.checkDone();
|
|
877
|
-
return remember(expressionCache,
|
|
1093
|
+
return remember(expressionCache, key, { ast, errors: p.errors });
|
|
878
1094
|
}
|
|
879
|
-
export function parseUnaryTests(input) {
|
|
880
|
-
const
|
|
1095
|
+
export function parseUnaryTests(input, options) {
|
|
1096
|
+
const key = cacheKey(input, options);
|
|
1097
|
+
const cached = unaryTestsCache.get(key);
|
|
881
1098
|
if (cached !== undefined)
|
|
882
1099
|
return cached;
|
|
883
1100
|
if (input.trim() === "-") {
|
|
884
|
-
return remember(unaryTestsCache,
|
|
1101
|
+
return remember(unaryTestsCache, key, {
|
|
885
1102
|
ast: { kind: "any-input", start: 0, end: input.length },
|
|
886
1103
|
errors: [],
|
|
887
1104
|
});
|
|
888
1105
|
}
|
|
889
|
-
const p = new Parser(input);
|
|
1106
|
+
const p = new Parser(input, options);
|
|
890
1107
|
const ast = p.parseUnaryTests();
|
|
891
|
-
return remember(unaryTestsCache,
|
|
1108
|
+
return remember(unaryTestsCache, key, { ast, errors: p.errors });
|
|
892
1109
|
}
|
|
893
1110
|
//# sourceMappingURL=parser.js.map
|
package/dist/types.js
CHANGED
|
@@ -67,6 +67,8 @@ export function getProperty(v, key) {
|
|
|
67
67
|
return v.month;
|
|
68
68
|
if (key === "day")
|
|
69
69
|
return v.day;
|
|
70
|
+
if (key === "weekday")
|
|
71
|
+
return weekdayOf(v);
|
|
70
72
|
return null;
|
|
71
73
|
}
|
|
72
74
|
if (isFeelTime(v)) {
|
|
@@ -77,7 +79,7 @@ export function getProperty(v, key) {
|
|
|
77
79
|
if (key === "second")
|
|
78
80
|
return v.second;
|
|
79
81
|
if (key === "time offset")
|
|
80
|
-
return v.offsetSeconds
|
|
82
|
+
return offsetDuration(v.offsetSeconds);
|
|
81
83
|
if (key === "timezone")
|
|
82
84
|
return v.timezone ?? null;
|
|
83
85
|
return null;
|
|
@@ -96,11 +98,13 @@ export function getProperty(v, key) {
|
|
|
96
98
|
if (key === "second")
|
|
97
99
|
return v.time.second;
|
|
98
100
|
if (key === "time offset")
|
|
99
|
-
return v.time.offsetSeconds
|
|
101
|
+
return offsetDuration(v.time.offsetSeconds);
|
|
100
102
|
if (key === "timezone")
|
|
101
103
|
return v.time.timezone ?? null;
|
|
102
104
|
if (key === "time")
|
|
103
105
|
return v.time;
|
|
106
|
+
if (key === "weekday")
|
|
107
|
+
return weekdayOf(v.date);
|
|
104
108
|
return null;
|
|
105
109
|
}
|
|
106
110
|
if (isFeelDayTimeDuration(v)) {
|
|
@@ -122,8 +126,37 @@ export function getProperty(v, key) {
|
|
|
122
126
|
return v.months % 12;
|
|
123
127
|
return null;
|
|
124
128
|
}
|
|
129
|
+
if (isFeelRange(v)) {
|
|
130
|
+
if (key === "start")
|
|
131
|
+
return v.start;
|
|
132
|
+
if (key === "end")
|
|
133
|
+
return v.end;
|
|
134
|
+
if (key === "start included")
|
|
135
|
+
return v.startIncluded;
|
|
136
|
+
if (key === "end included")
|
|
137
|
+
return v.endIncluded;
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
125
140
|
// FeelContext
|
|
126
141
|
const val = v[key];
|
|
127
142
|
return val !== undefined ? val : null;
|
|
128
143
|
}
|
|
144
|
+
/** A time offset is a duration, not a number of hours. */
|
|
145
|
+
function offsetDuration(offsetSeconds) {
|
|
146
|
+
if (offsetSeconds === undefined)
|
|
147
|
+
return null;
|
|
148
|
+
return { type: "days-time-duration", seconds: offsetSeconds };
|
|
149
|
+
}
|
|
150
|
+
/** Day of the week as FEEL numbers them: Monday is 1, Sunday is 7. */
|
|
151
|
+
function weekdayOf(d) {
|
|
152
|
+
const y = d.year - 1;
|
|
153
|
+
let days = 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400);
|
|
154
|
+
const monthLengths = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
155
|
+
const leap = (d.year % 4 === 0 && d.year % 100 !== 0) || d.year % 400 === 0;
|
|
156
|
+
for (let m = 1; m < d.month; m++)
|
|
157
|
+
days += (m === 2 && leap ? 29 : monthLengths[m]) ?? 30;
|
|
158
|
+
days += d.day;
|
|
159
|
+
// 0001-01-01 was a Monday in the proleptic Gregorian calendar.
|
|
160
|
+
return ((days - 1) % 7) + 1;
|
|
161
|
+
}
|
|
129
162
|
//# sourceMappingURL=types.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bpmnkit/feel",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -44,6 +44,9 @@
|
|
|
44
44
|
"build": "tsc",
|
|
45
45
|
"typecheck": "tsc --noEmit",
|
|
46
46
|
"check": "biome check .",
|
|
47
|
-
"test": "vitest run"
|
|
47
|
+
"test": "vitest run",
|
|
48
|
+
"tck:extract": "node tasks/extract-tck-tests.mjs",
|
|
49
|
+
"tck": "node tasks/extract-tck-tests.mjs && vitest run tests/tck.test.ts",
|
|
50
|
+
"tck:run": "vitest run tests/tck.test.ts"
|
|
48
51
|
}
|
|
49
52
|
}
|