@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/evaluator.d.ts
CHANGED
|
@@ -6,7 +6,10 @@ export interface EvalContext {
|
|
|
6
6
|
input?: FeelValue;
|
|
7
7
|
}
|
|
8
8
|
export declare function evaluate(node: FeelNode, ctx: EvalContext): FeelValue;
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Evaluates a unary test against an input value. A decision table's rule
|
|
11
|
+
* either matches or it does not, so an unknown answer is not a match.
|
|
12
|
+
*/
|
|
10
13
|
export declare function evaluateUnaryTest(node: FeelNode, input: FeelValue, ctx: EvalContext): boolean;
|
|
11
14
|
/** Evaluate a full unary-test node (the root returned by parseUnaryTests). */
|
|
12
15
|
export declare function evaluateUnaryTests(node: FeelNode, input: FeelValue, ctx: EvalContext): boolean;
|
package/dist/evaluator.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { compareValues, getBuiltin, parseTemporal } from "./builtins.js";
|
|
2
|
-
import { getProperty, isFeelContext, isFeelDate, isFeelDateTime, isFeelDayTimeDuration, isFeelList, isFeelRange, isFeelTime, isFeelYearsMonthsDuration, } from "./types.js";
|
|
1
|
+
import { compareValues, getBuiltin, orderNamedArgs, parseTemporal } from "./builtins.js";
|
|
2
|
+
import { getProperty, isFeelContext, isFeelDate, isFeelDateTime, isFeelDayTimeDuration, isFeelFunction, isFeelList, isFeelRange, isFeelTime, isFeelYearsMonthsDuration, } from "./types.js";
|
|
3
3
|
function lookupVar(ctx, name) {
|
|
4
4
|
if (name === "?")
|
|
5
5
|
return ctx.input ?? null;
|
|
@@ -13,16 +13,63 @@ function lookupVar(ctx, name) {
|
|
|
13
13
|
function childCtx(parent, vars = {}) {
|
|
14
14
|
return { vars, parent, input: parent.input };
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* The scope a filter condition runs in. The element is bound to `item`, and
|
|
18
|
+
* when it is a context its entries are in scope directly, so a list of
|
|
19
|
+
* records filters on their fields: `[{a: 1}, {a: 2}][a >= 2]`. An entry
|
|
20
|
+
* called `item` is the element's own, not the element.
|
|
21
|
+
*/
|
|
22
|
+
function filterCtx(parent, item) {
|
|
23
|
+
const vars = { item };
|
|
24
|
+
if (isFeelContext(item))
|
|
25
|
+
Object.assign(vars, item);
|
|
26
|
+
return childCtx(parent, vars);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Expands the domain of a `for`/`some`/`every` binding into the values to
|
|
30
|
+
* iterate. A numeric or date range yields its whole span, counting down when
|
|
31
|
+
* it runs backwards; anything else iterates as a single-element list. A range
|
|
32
|
+
* over values that cannot be stepped through has no iteration, which is null.
|
|
33
|
+
*/
|
|
34
|
+
function iterationValues(domain) {
|
|
35
|
+
if (isFeelList(domain))
|
|
36
|
+
return domain;
|
|
37
|
+
if (!isFeelRange(domain))
|
|
38
|
+
return [domain];
|
|
39
|
+
const { start, end } = domain;
|
|
40
|
+
if (typeof start === "number" && typeof end === "number") {
|
|
41
|
+
return span(start, end).map((n) => n);
|
|
42
|
+
}
|
|
43
|
+
if (isFeelDate(start) && isFeelDate(end)) {
|
|
44
|
+
return span(dateToEpochDays(start), dateToEpochDays(end)).map((d) => epochDaysToDate(d));
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
/** Every integer from `from` to `to`, in whichever direction that runs. */
|
|
49
|
+
function span(from, to) {
|
|
50
|
+
const step = from <= to ? 1 : -1;
|
|
51
|
+
const values = [];
|
|
52
|
+
for (let v = from; step > 0 ? v <= to : v >= to; v += step)
|
|
53
|
+
values.push(v);
|
|
54
|
+
return values;
|
|
55
|
+
}
|
|
16
56
|
// -------------------------------------------------------------------------
|
|
17
57
|
// Arithmetic helpers for temporal types
|
|
18
58
|
// -------------------------------------------------------------------------
|
|
59
|
+
/**
|
|
60
|
+
* Shifts a date by whole months. The day is clamped to the length of the
|
|
61
|
+
* month it lands in, so 2020-01-31 plus P1M is 2020-02-29 rather than a
|
|
62
|
+
* February 31st that no calendar has.
|
|
63
|
+
*/
|
|
64
|
+
function shiftMonths(date, months) {
|
|
65
|
+
const total = date.month + months;
|
|
66
|
+
const year = date.year + Math.floor((total - 1) / 12);
|
|
67
|
+
const month = ((((total - 1) % 12) + 12) % 12) + 1;
|
|
68
|
+
return { type: "date", year, month, day: Math.min(date.day, DAYS_IN_MONTH_TABLE(year, month)) };
|
|
69
|
+
}
|
|
19
70
|
function addDuration(date, dur) {
|
|
20
71
|
if (isFeelDate(date) && isFeelYearsMonthsDuration(dur)) {
|
|
21
|
-
|
|
22
|
-
let y = date.year;
|
|
23
|
-
y += Math.floor((m - 1) / 12);
|
|
24
|
-
m = ((m - 1 + 1200) % 12) + 1;
|
|
25
|
-
return { type: "date", year: y, month: m, day: date.day };
|
|
72
|
+
return shiftMonths(date, dur.months);
|
|
26
73
|
}
|
|
27
74
|
if (isFeelDate(date) && isFeelDayTimeDuration(dur)) {
|
|
28
75
|
const EPOCH = dateToEpochDays(date);
|
|
@@ -34,16 +81,7 @@ function addDuration(date, dur) {
|
|
|
34
81
|
return epochSecondsToDateTime(totalSec, date.time.offsetSeconds, date.time.timezone);
|
|
35
82
|
}
|
|
36
83
|
if (isFeelDateTime(date) && isFeelYearsMonthsDuration(dur)) {
|
|
37
|
-
|
|
38
|
-
let m = d.month + dur.months;
|
|
39
|
-
let y = d.year;
|
|
40
|
-
y += Math.floor((m - 1) / 12);
|
|
41
|
-
m = ((m - 1 + 1200) % 12) + 1;
|
|
42
|
-
return {
|
|
43
|
-
type: "date-time",
|
|
44
|
-
date: { type: "date", year: y, month: m, day: d.day },
|
|
45
|
-
time: date.time,
|
|
46
|
-
};
|
|
84
|
+
return { type: "date-time", date: shiftMonths(date.date, dur.months), time: date.time };
|
|
47
85
|
}
|
|
48
86
|
if (isFeelDayTimeDuration(date) && isFeelDayTimeDuration(dur)) {
|
|
49
87
|
return { type: "days-time-duration", seconds: date.seconds + dur.seconds };
|
|
@@ -178,9 +216,16 @@ export function evaluate(node, ctx) {
|
|
|
178
216
|
case "list":
|
|
179
217
|
return node.items.map((item) => evaluate(item, ctx));
|
|
180
218
|
case "context": {
|
|
219
|
+
// Entries are evaluated in order in a scope that already holds the
|
|
220
|
+
// preceding ones, so `{a: 1, b: a + 1}` resolves `a` in `b`.
|
|
181
221
|
const result = {};
|
|
222
|
+
const entryCtx = childCtx(ctx, result);
|
|
182
223
|
for (const entry of node.entries) {
|
|
183
|
-
|
|
224
|
+
// A key given twice names two different values, which is not a
|
|
225
|
+
// context at all.
|
|
226
|
+
if (entry.key in result)
|
|
227
|
+
return null;
|
|
228
|
+
result[entry.key] = evaluate(entry.value, entryCtx);
|
|
184
229
|
}
|
|
185
230
|
return result;
|
|
186
231
|
}
|
|
@@ -206,71 +251,53 @@ export function evaluate(node, ctx) {
|
|
|
206
251
|
return getProperty(base, node.key);
|
|
207
252
|
}
|
|
208
253
|
case "filter": {
|
|
209
|
-
const
|
|
210
|
-
if (
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return typeof result === "number" ? [base] : result ? [base] : [];
|
|
216
|
-
}
|
|
254
|
+
const value = evaluate(node.base, ctx);
|
|
255
|
+
if (value === null)
|
|
256
|
+
return [];
|
|
257
|
+
// A value that is not a list is filtered as a list holding just it,
|
|
258
|
+
// so `true[1]` is true and `true[0]` is null.
|
|
259
|
+
const base = isFeelList(value) ? value : [value];
|
|
217
260
|
// Numeric index filter
|
|
218
|
-
const first = evaluate(node.condition,
|
|
261
|
+
const first = evaluate(node.condition, filterCtx(ctx, base[0] ?? null));
|
|
219
262
|
if (typeof first === "number") {
|
|
220
263
|
const idx = first > 0 ? first - 1 : base.length + first;
|
|
221
264
|
const val = base[Math.floor(idx)];
|
|
222
265
|
return val !== undefined ? val : null;
|
|
223
266
|
}
|
|
224
|
-
return base.filter((item) =>
|
|
225
|
-
const r = evaluate(node.condition, childCtx(ctx, { item }));
|
|
226
|
-
return r === true || (r !== false && r !== null);
|
|
227
|
-
});
|
|
267
|
+
return base.filter((item) => evaluate(node.condition, filterCtx(ctx, item)) === true);
|
|
228
268
|
}
|
|
229
269
|
case "call":
|
|
230
270
|
return evalCall(node.callee, node.args, ctx);
|
|
271
|
+
case "call-expr": {
|
|
272
|
+
const target = evaluate(node.target, ctx);
|
|
273
|
+
if (!isFeelFunction(target))
|
|
274
|
+
return null;
|
|
275
|
+
return target.call(node.args.map((a) => evaluate(a, ctx)));
|
|
276
|
+
}
|
|
231
277
|
case "call-named": {
|
|
278
|
+
const argNames = node.args.map((a) => a.name);
|
|
279
|
+
const values = node.args.map((a) => evaluate(a.value, ctx));
|
|
232
280
|
const builtin = getBuiltin(node.callee);
|
|
233
|
-
if (
|
|
234
|
-
const
|
|
235
|
-
if (
|
|
281
|
+
if (builtin) {
|
|
282
|
+
const order = orderNamedArgs(node.callee, argNames);
|
|
283
|
+
if (order === null)
|
|
236
284
|
return null;
|
|
237
|
-
|
|
238
|
-
return fn.call(args);
|
|
285
|
+
return builtin.call(order.map((idx) => values[idx] ?? null));
|
|
239
286
|
}
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
287
|
+
const fn = lookupVar(ctx, node.callee);
|
|
288
|
+
if (!isFeelFunction(fn))
|
|
289
|
+
return null;
|
|
290
|
+
return fn.call(orderArgs(fn.paramNames, argNames, values));
|
|
243
291
|
}
|
|
244
292
|
case "if": {
|
|
245
293
|
const cond = evaluate(node.condition, ctx);
|
|
246
294
|
return cond === true ? evaluate(node.then, ctx) : evaluate(node.else, ctx);
|
|
247
295
|
}
|
|
248
|
-
case "for":
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
254
|
-
const partial = [];
|
|
255
|
-
const results = evalCartesian(node.bindings, domains, 0, ctx, node, partial);
|
|
256
|
-
return results;
|
|
257
|
-
}
|
|
258
|
-
case "some": {
|
|
259
|
-
const domains = [];
|
|
260
|
-
for (const binding of node.bindings) {
|
|
261
|
-
const d = evaluate(binding.domain, ctx);
|
|
262
|
-
domains.push(isFeelList(d) ? d : [d]);
|
|
263
|
-
}
|
|
264
|
-
return evalQuantifier("some", node.bindings, domains, 0, ctx, node.satisfies);
|
|
265
|
-
}
|
|
266
|
-
case "every": {
|
|
267
|
-
const domains = [];
|
|
268
|
-
for (const binding of node.bindings) {
|
|
269
|
-
const d = evaluate(binding.domain, ctx);
|
|
270
|
-
domains.push(isFeelList(d) ? d : [d]);
|
|
271
|
-
}
|
|
272
|
-
return evalQuantifier("every", node.bindings, domains, 0, ctx, node.satisfies);
|
|
273
|
-
}
|
|
296
|
+
case "for":
|
|
297
|
+
return evalFor(node.bindings, node.body, ctx);
|
|
298
|
+
case "some":
|
|
299
|
+
case "every":
|
|
300
|
+
return evalQuantifier(node.kind, node.bindings, node.satisfies, ctx);
|
|
274
301
|
case "between": {
|
|
275
302
|
const val = evaluate(node.value, ctx);
|
|
276
303
|
const low = evaluate(node.low, ctx);
|
|
@@ -281,11 +308,11 @@ export function evaluate(node, ctx) {
|
|
|
281
308
|
return null;
|
|
282
309
|
return cmpLow >= 0 && cmpHigh <= 0;
|
|
283
310
|
}
|
|
284
|
-
case "in-test":
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
311
|
+
case "in-test":
|
|
312
|
+
// The right-hand side is a unary test, evaluated with the left-hand
|
|
313
|
+
// value as its implicit input. Unlike a decision table's test, this
|
|
314
|
+
// one keeps an unknown answer unknown.
|
|
315
|
+
return unaryTestValue(node.test, evaluate(node.value, ctx), ctx);
|
|
289
316
|
case "instance-of": {
|
|
290
317
|
const val = evaluate(node.value, ctx);
|
|
291
318
|
return checkInstanceOf(val, node.typeName);
|
|
@@ -333,9 +360,8 @@ function evalBinary(op, leftNode, rightNode, ctx) {
|
|
|
333
360
|
const r = evaluate(rightNode, ctx);
|
|
334
361
|
if (r === false)
|
|
335
362
|
return false;
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return true;
|
|
363
|
+
// Anything that is not a boolean leaves the result unknown.
|
|
364
|
+
return l === true && r === true ? true : null;
|
|
339
365
|
}
|
|
340
366
|
if (op === "or") {
|
|
341
367
|
const l = evaluate(leftNode, ctx);
|
|
@@ -344,16 +370,21 @@ function evalBinary(op, leftNode, rightNode, ctx) {
|
|
|
344
370
|
const r = evaluate(rightNode, ctx);
|
|
345
371
|
if (r === true)
|
|
346
372
|
return true;
|
|
347
|
-
|
|
348
|
-
return null;
|
|
349
|
-
return false;
|
|
373
|
+
return l === false && r === false ? false : null;
|
|
350
374
|
}
|
|
351
375
|
const left = evaluate(leftNode, ctx);
|
|
352
376
|
const right = evaluate(rightNode, ctx);
|
|
353
|
-
if (op === "=")
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
377
|
+
if (op === "=" || op === "!=") {
|
|
378
|
+
// Comparing values of different types says nothing, so it is null
|
|
379
|
+
// rather than false. Comparing against null stays a real answer.
|
|
380
|
+
if (left !== null && right !== null && typeTag(left) !== typeTag(right))
|
|
381
|
+
return null;
|
|
382
|
+
// Temporal values are equal when they name the same point, however
|
|
383
|
+
// each was written: 12:00-01:00 and 17:00+04:00 are one instant.
|
|
384
|
+
const instant = isTemporal(left) ? compareValues(left, right) : null;
|
|
385
|
+
const equal = instant !== null ? instant === 0 : deepEqual(left, right);
|
|
386
|
+
return op === "=" ? equal : !equal;
|
|
387
|
+
}
|
|
357
388
|
if (left === null || right === null)
|
|
358
389
|
return null;
|
|
359
390
|
if (op === "+" || op === "-") {
|
|
@@ -409,6 +440,19 @@ function evalBinary(op, leftNode, rightNode, ctx) {
|
|
|
409
440
|
return cmp >= 0;
|
|
410
441
|
return null;
|
|
411
442
|
}
|
|
443
|
+
/**
|
|
444
|
+
* Orders the arguments of a named invocation of a user-defined function. A
|
|
445
|
+
* function that declares no parameter names, or that is handed a name it does
|
|
446
|
+
* not declare, gets nulls rather than a silently mis-ordered argument list.
|
|
447
|
+
*/
|
|
448
|
+
function orderArgs(paramNames, argNames, values) {
|
|
449
|
+
if (!paramNames)
|
|
450
|
+
return [];
|
|
451
|
+
return paramNames.map((param) => {
|
|
452
|
+
const idx = argNames.indexOf(param);
|
|
453
|
+
return idx >= 0 ? (values[idx] ?? null) : null;
|
|
454
|
+
});
|
|
455
|
+
}
|
|
412
456
|
function evalCall(callee, argNodes, ctx) {
|
|
413
457
|
const builtin = getBuiltin(callee);
|
|
414
458
|
if (builtin) {
|
|
@@ -421,63 +465,100 @@ function evalCall(callee, argNodes, ctx) {
|
|
|
421
465
|
const args = argNodes.map((a) => evaluate(a, ctx));
|
|
422
466
|
return fn.call(args);
|
|
423
467
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
const result = evaluate(forNode.body, childCtx(ctx, vars));
|
|
431
|
-
partial.push(result);
|
|
432
|
-
return [result];
|
|
433
|
-
}
|
|
434
|
-
const binding = bindings[idx];
|
|
435
|
-
if (!binding)
|
|
436
|
-
return [];
|
|
437
|
-
const domain = domains[idx] ?? [];
|
|
468
|
+
/**
|
|
469
|
+
* Evaluates a `for`. Each binding's domain is evaluated with the bindings to
|
|
470
|
+
* its left already in scope, so `for x in xs, y in x` works, and the body sees
|
|
471
|
+
* the results produced so far as `partial`.
|
|
472
|
+
*/
|
|
473
|
+
function evalFor(bindings, body, ctx) {
|
|
438
474
|
const results = [];
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
vars[b.name] = partial[i] ?? null;
|
|
475
|
+
let failed = false;
|
|
476
|
+
const iterate = (idx, scope) => {
|
|
477
|
+
if (failed)
|
|
478
|
+
return;
|
|
479
|
+
if (idx === bindings.length) {
|
|
480
|
+
results.push(evaluate(body, childCtx(scope, { partial: [...results] })));
|
|
481
|
+
return;
|
|
447
482
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
483
|
+
const binding = bindings[idx];
|
|
484
|
+
if (!binding)
|
|
485
|
+
return;
|
|
486
|
+
const domain = iterationValues(evaluate(binding.domain, scope));
|
|
487
|
+
if (domain === null) {
|
|
488
|
+
failed = true;
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
for (const value of domain) {
|
|
492
|
+
iterate(idx + 1, childCtx(scope, { [binding.name]: value }));
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
iterate(0, ctx);
|
|
496
|
+
return failed ? null : results;
|
|
455
497
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
498
|
+
/**
|
|
499
|
+
* Evaluates `some`/`every`. A definite answer wins over an unknown one: one
|
|
500
|
+
* true satisfies `some` whatever else the domain holds, and one false settles
|
|
501
|
+
* `every`. Otherwise an unknown anywhere makes the whole answer unknown.
|
|
502
|
+
*/
|
|
503
|
+
function evalQuantifier(kind, bindings, satisfies, ctx) {
|
|
504
|
+
let sawTrue = false;
|
|
505
|
+
let sawFalse = false;
|
|
506
|
+
let sawUnknown = false;
|
|
507
|
+
let failed = false;
|
|
508
|
+
const visit = (idx, scope) => {
|
|
509
|
+
if (failed)
|
|
510
|
+
return;
|
|
511
|
+
if (idx === bindings.length) {
|
|
512
|
+
const result = asBoolean(evaluate(satisfies, scope));
|
|
513
|
+
if (result === true)
|
|
514
|
+
sawTrue = true;
|
|
515
|
+
else if (result === false)
|
|
516
|
+
sawFalse = true;
|
|
517
|
+
else
|
|
518
|
+
sawUnknown = true;
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const binding = bindings[idx];
|
|
522
|
+
if (!binding)
|
|
523
|
+
return;
|
|
524
|
+
const domain = iterationValues(evaluate(binding.domain, scope));
|
|
525
|
+
if (domain === null) {
|
|
526
|
+
failed = true;
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
for (const value of domain) {
|
|
530
|
+
visit(idx + 1, childCtx(scope, { [binding.name]: value }));
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
visit(0, ctx);
|
|
534
|
+
if (failed)
|
|
535
|
+
return null;
|
|
478
536
|
if (kind === "some")
|
|
479
|
-
return
|
|
480
|
-
return
|
|
537
|
+
return sawTrue ? true : sawUnknown ? null : false;
|
|
538
|
+
return sawFalse ? false : sawUnknown ? null : true;
|
|
539
|
+
}
|
|
540
|
+
function asBoolean(v) {
|
|
541
|
+
return typeof v === "boolean" ? v : null;
|
|
542
|
+
}
|
|
543
|
+
/** True for the values that name a point in time or a length of it. */
|
|
544
|
+
function isTemporal(v) {
|
|
545
|
+
return (isFeelDate(v) ||
|
|
546
|
+
isFeelTime(v) ||
|
|
547
|
+
isFeelDateTime(v) ||
|
|
548
|
+
isFeelDayTimeDuration(v) ||
|
|
549
|
+
isFeelYearsMonthsDuration(v));
|
|
550
|
+
}
|
|
551
|
+
/** The FEEL type of a value, for deciding whether two values are comparable. */
|
|
552
|
+
function typeTag(v) {
|
|
553
|
+
if (v === null)
|
|
554
|
+
return "null";
|
|
555
|
+
if (Array.isArray(v))
|
|
556
|
+
return "list";
|
|
557
|
+
const t = typeof v;
|
|
558
|
+
if (t !== "object")
|
|
559
|
+
return t;
|
|
560
|
+
const tagged = v.type;
|
|
561
|
+
return typeof tagged === "string" ? tagged : "context";
|
|
481
562
|
}
|
|
482
563
|
function deepEqual(a, b) {
|
|
483
564
|
if (a === b)
|
|
@@ -511,6 +592,10 @@ function deepEqual(a, b) {
|
|
|
511
592
|
return false;
|
|
512
593
|
}
|
|
513
594
|
function testIncludes(test, val) {
|
|
595
|
+
// Two lists are compared, not searched: [1,2,3] is a member of
|
|
596
|
+
// [[1,2,3,4], [1,2,3]] rather than of its elements.
|
|
597
|
+
if (isFeelList(test) && isFeelList(val))
|
|
598
|
+
return deepEqual(test, val);
|
|
514
599
|
if (isFeelRange(test)) {
|
|
515
600
|
const cmpStart = compareValues(val, test.start);
|
|
516
601
|
const cmpEnd = compareValues(val, test.end);
|
|
@@ -531,6 +616,10 @@ function testIncludes(test, val) {
|
|
|
531
616
|
return deepEqual(val, test);
|
|
532
617
|
}
|
|
533
618
|
function checkInstanceOf(val, typeName) {
|
|
619
|
+
// null is not an instance of anything, Any included: it is the absence of
|
|
620
|
+
// a value rather than a value of some type.
|
|
621
|
+
if (val === null)
|
|
622
|
+
return typeName === "null" || typeName === "Null";
|
|
534
623
|
switch (typeName) {
|
|
535
624
|
case "number":
|
|
536
625
|
return typeof val === "number";
|
|
@@ -545,8 +634,10 @@ function checkInstanceOf(val, typeName) {
|
|
|
545
634
|
case "date and time":
|
|
546
635
|
return isFeelDateTime(val);
|
|
547
636
|
case "days and time duration":
|
|
637
|
+
case "dayTimeDuration":
|
|
548
638
|
return isFeelDayTimeDuration(val);
|
|
549
639
|
case "years and months duration":
|
|
640
|
+
case "yearMonthDuration":
|
|
550
641
|
return isFeelYearsMonthsDuration(val);
|
|
551
642
|
case "list":
|
|
552
643
|
return Array.isArray(val);
|
|
@@ -555,25 +646,39 @@ function checkInstanceOf(val, typeName) {
|
|
|
555
646
|
case "function":
|
|
556
647
|
return typeof val === "object" && val !== null && "call" in val;
|
|
557
648
|
case "Any":
|
|
649
|
+
case "any":
|
|
558
650
|
return true;
|
|
559
651
|
case "null":
|
|
560
|
-
|
|
652
|
+
case "Null":
|
|
653
|
+
return false;
|
|
561
654
|
default:
|
|
562
655
|
return false;
|
|
563
656
|
}
|
|
564
657
|
}
|
|
565
|
-
/**
|
|
566
|
-
|
|
658
|
+
/**
|
|
659
|
+
* Evaluates a unary test, keeping an unknown answer unknown. A range whose
|
|
660
|
+
* bound is null, or an input of null, says nothing about membership.
|
|
661
|
+
*/
|
|
662
|
+
function unaryTestValue(node, input, ctx) {
|
|
567
663
|
const withInput = { ...ctx, input };
|
|
568
664
|
const result = evaluate(node, withInput);
|
|
569
665
|
if (typeof result === "boolean")
|
|
570
666
|
return result;
|
|
571
667
|
// Range result in unary-test context → membership test
|
|
572
668
|
if (isFeelRange(result))
|
|
573
|
-
return testIncludes(result, input)
|
|
669
|
+
return testIncludes(result, input);
|
|
574
670
|
// List result → any element matches
|
|
575
|
-
if (isFeelList(result))
|
|
576
|
-
|
|
671
|
+
if (isFeelList(result)) {
|
|
672
|
+
let unknown = false;
|
|
673
|
+
for (const item of result) {
|
|
674
|
+
const match = testIncludes(item, input);
|
|
675
|
+
if (match === true)
|
|
676
|
+
return true;
|
|
677
|
+
if (match === null)
|
|
678
|
+
unknown = true;
|
|
679
|
+
}
|
|
680
|
+
return unknown ? null : false;
|
|
681
|
+
}
|
|
577
682
|
// A plain expression in unary test mode is an equality test.
|
|
578
683
|
// When the result is null: only match if the node itself is the null literal
|
|
579
684
|
// (null arithmetic in comparisons also yields null but must not match anything).
|
|
@@ -581,6 +686,13 @@ export function evaluateUnaryTest(node, input, ctx) {
|
|
|
581
686
|
return deepEqual(result, input);
|
|
582
687
|
return node.kind === "null" ? input === null : false;
|
|
583
688
|
}
|
|
689
|
+
/**
|
|
690
|
+
* Evaluates a unary test against an input value. A decision table's rule
|
|
691
|
+
* either matches or it does not, so an unknown answer is not a match.
|
|
692
|
+
*/
|
|
693
|
+
export function evaluateUnaryTest(node, input, ctx) {
|
|
694
|
+
return unaryTestValue(node, input, ctx) === true;
|
|
695
|
+
}
|
|
584
696
|
/** Evaluate a full unary-test node (the root returned by parseUnaryTests). */
|
|
585
697
|
export function evaluateUnaryTests(node, input, ctx) {
|
|
586
698
|
return evaluateUnaryTest(node, input, ctx);
|
package/dist/formatter.js
CHANGED
|
@@ -6,6 +6,17 @@ export function formatFeel(node, opts) {
|
|
|
6
6
|
const o = { ...DEFAULTS, ...opts };
|
|
7
7
|
return fmt(node, o, 0);
|
|
8
8
|
}
|
|
9
|
+
const STRING_ESCAPES = {
|
|
10
|
+
"\\": "\\\\",
|
|
11
|
+
'"': '\\"',
|
|
12
|
+
"\n": "\\n",
|
|
13
|
+
"\r": "\\r",
|
|
14
|
+
"\t": "\\t",
|
|
15
|
+
};
|
|
16
|
+
/** Re-escapes a string literal's value, which the parser stores decoded. */
|
|
17
|
+
function escapeString(value) {
|
|
18
|
+
return value.replace(/[\\"\n\r\t]/g, (c) => STRING_ESCAPES[c] ?? c);
|
|
19
|
+
}
|
|
9
20
|
function fmt(node, o, depth) {
|
|
10
21
|
const ind = o.indent.repeat(depth);
|
|
11
22
|
const ind1 = o.indent.repeat(depth + 1);
|
|
@@ -17,7 +28,7 @@ function fmt(node, o, depth) {
|
|
|
17
28
|
case "number":
|
|
18
29
|
return String(node.value);
|
|
19
30
|
case "string":
|
|
20
|
-
return `"${node.value
|
|
31
|
+
return `"${escapeString(node.value)}"`;
|
|
21
32
|
case "temporal":
|
|
22
33
|
return node.raw;
|
|
23
34
|
case "name":
|
|
@@ -64,6 +75,10 @@ function fmt(node, o, depth) {
|
|
|
64
75
|
const args = node.args.map((a) => fmt(a, o, depth)).join(", ");
|
|
65
76
|
return `${node.callee}(${args})`;
|
|
66
77
|
}
|
|
78
|
+
case "call-expr": {
|
|
79
|
+
const args = node.args.map((a) => fmt(a, o, depth)).join(", ");
|
|
80
|
+
return `${fmt(node.target, o, depth)}(${args})`;
|
|
81
|
+
}
|
|
67
82
|
case "call-named": {
|
|
68
83
|
const args = node.args.map((a) => `${a.name}: ${fmt(a.value, o, depth)}`).join(", ");
|
|
69
84
|
return `${node.callee}(${args})`;
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ export { tokenize } from "./lexer.js";
|
|
|
4
4
|
export type { FeelToken, FeelTokenKind } from "./lexer.js";
|
|
5
5
|
export type { FeelNode, BinaryOp } from "./ast.js";
|
|
6
6
|
export { parseExpression, parseUnaryTests } from "./parser.js";
|
|
7
|
-
export type { ParseResult, ParseError } from "./parser.js";
|
|
7
|
+
export type { ParseResult, ParseError, ParseOptions } from "./parser.js";
|
|
8
8
|
export { evaluate, evaluateUnaryTests, evaluateUnaryTest } from "./evaluator.js";
|
|
9
9
|
export type { EvalContext } from "./evaluator.js";
|
|
10
10
|
export { formatFeel } from "./formatter.js";
|
package/dist/lexer.d.ts
CHANGED
|
@@ -6,4 +6,11 @@ export interface FeelToken {
|
|
|
6
6
|
end: number;
|
|
7
7
|
}
|
|
8
8
|
export declare function tokenize(input: string): FeelToken[];
|
|
9
|
+
/**
|
|
10
|
+
* Decodes the escape sequences of a FEEL string literal body (the text between
|
|
11
|
+
* the quotes). Recognizes \' \" \\ \n \r \t, \uXXXX and the extended
|
|
12
|
+
* \UXXXXXX form; an unrecognized sequence is left as written, since dropping
|
|
13
|
+
* the backslash would silently alter the author's data.
|
|
14
|
+
*/
|
|
15
|
+
export declare function unescapeString(body: string): string;
|
|
9
16
|
//# sourceMappingURL=lexer.d.ts.map
|