@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
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { FeelNode } from "./ast.js";
|
|
2
|
+
import type { FeelValue } from "./types.js";
|
|
3
|
+
export interface EvalContext {
|
|
4
|
+
vars: Record<string, FeelValue>;
|
|
5
|
+
parent?: EvalContext;
|
|
6
|
+
input?: FeelValue;
|
|
7
|
+
}
|
|
8
|
+
export declare function evaluate(node: FeelNode, ctx: EvalContext): FeelValue;
|
|
9
|
+
/** Evaluate a unary test against an input value. Returns boolean. */
|
|
10
|
+
export declare function evaluateUnaryTest(node: FeelNode, input: FeelValue, ctx: EvalContext): boolean;
|
|
11
|
+
/** Evaluate a full unary-test node (the root returned by parseUnaryTests). */
|
|
12
|
+
export declare function evaluateUnaryTests(node: FeelNode, input: FeelValue, ctx: EvalContext): boolean;
|
|
13
|
+
//# sourceMappingURL=evaluator.d.ts.map
|
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
import { compareValues, getBuiltin, parseTemporal } from "./builtins.js";
|
|
2
|
+
import { getProperty, isFeelContext, isFeelDate, isFeelDateTime, isFeelDayTimeDuration, isFeelList, isFeelRange, isFeelTime, isFeelYearsMonthsDuration, } from "./types.js";
|
|
3
|
+
function lookupVar(ctx, name) {
|
|
4
|
+
if (name === "?")
|
|
5
|
+
return ctx.input ?? null;
|
|
6
|
+
const v = ctx.vars[name];
|
|
7
|
+
if (v !== undefined)
|
|
8
|
+
return v;
|
|
9
|
+
if (ctx.parent)
|
|
10
|
+
return lookupVar(ctx.parent, name);
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
function childCtx(parent, vars = {}) {
|
|
14
|
+
return { vars, parent, input: parent.input };
|
|
15
|
+
}
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
// Arithmetic helpers for temporal types
|
|
18
|
+
// -------------------------------------------------------------------------
|
|
19
|
+
function addDuration(date, dur) {
|
|
20
|
+
if (isFeelDate(date) && isFeelYearsMonthsDuration(dur)) {
|
|
21
|
+
let m = date.month + dur.months;
|
|
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 };
|
|
26
|
+
}
|
|
27
|
+
if (isFeelDate(date) && isFeelDayTimeDuration(dur)) {
|
|
28
|
+
const EPOCH = dateToEpochDays(date);
|
|
29
|
+
const newEpoch = EPOCH + Math.floor(dur.seconds / 86400);
|
|
30
|
+
return epochDaysToDate(newEpoch);
|
|
31
|
+
}
|
|
32
|
+
if (isFeelDateTime(date) && isFeelDayTimeDuration(dur)) {
|
|
33
|
+
const totalSec = dateTimeToEpochSeconds(date) + dur.seconds;
|
|
34
|
+
return epochSecondsToDateTime(totalSec, date.time.offsetSeconds, date.time.timezone);
|
|
35
|
+
}
|
|
36
|
+
if (isFeelDateTime(date) && isFeelYearsMonthsDuration(dur)) {
|
|
37
|
+
const d = date.date;
|
|
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
|
+
};
|
|
47
|
+
}
|
|
48
|
+
if (isFeelDayTimeDuration(date) && isFeelDayTimeDuration(dur)) {
|
|
49
|
+
return { type: "days-time-duration", seconds: date.seconds + dur.seconds };
|
|
50
|
+
}
|
|
51
|
+
if (isFeelYearsMonthsDuration(date) && isFeelYearsMonthsDuration(dur)) {
|
|
52
|
+
return { type: "years-months-duration", months: date.months + dur.months };
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
function subtractValues(a, b) {
|
|
57
|
+
if (typeof a === "number" && typeof b === "number")
|
|
58
|
+
return a - b;
|
|
59
|
+
if (isFeelDate(a) && isFeelDate(b)) {
|
|
60
|
+
const diff = dateToEpochDays(a) - dateToEpochDays(b);
|
|
61
|
+
return { type: "days-time-duration", seconds: diff * 86400 };
|
|
62
|
+
}
|
|
63
|
+
if (isFeelDate(a) && isFeelDayTimeDuration(b)) {
|
|
64
|
+
return addDuration(a, { type: "days-time-duration", seconds: -b.seconds });
|
|
65
|
+
}
|
|
66
|
+
if (isFeelDate(a) && isFeelYearsMonthsDuration(b)) {
|
|
67
|
+
return addDuration(a, { type: "years-months-duration", months: -b.months });
|
|
68
|
+
}
|
|
69
|
+
if (isFeelDateTime(a) && isFeelDateTime(b)) {
|
|
70
|
+
const diff = dateTimeToEpochSeconds(a) - dateTimeToEpochSeconds(b);
|
|
71
|
+
return { type: "days-time-duration", seconds: diff };
|
|
72
|
+
}
|
|
73
|
+
if (isFeelDateTime(a) && isFeelDayTimeDuration(b)) {
|
|
74
|
+
return addDuration(a, { type: "days-time-duration", seconds: -b.seconds });
|
|
75
|
+
}
|
|
76
|
+
if (isFeelDateTime(a) && isFeelYearsMonthsDuration(b)) {
|
|
77
|
+
return addDuration(a, { type: "years-months-duration", months: -b.months });
|
|
78
|
+
}
|
|
79
|
+
if (isFeelDayTimeDuration(a) && isFeelDayTimeDuration(b)) {
|
|
80
|
+
return { type: "days-time-duration", seconds: a.seconds - b.seconds };
|
|
81
|
+
}
|
|
82
|
+
if (isFeelYearsMonthsDuration(a) && isFeelYearsMonthsDuration(b)) {
|
|
83
|
+
return { type: "years-months-duration", months: a.months - b.months };
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function DAYS_IN_MONTH_TABLE(y, m) {
|
|
88
|
+
const table = [0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
89
|
+
if (m === 2 && ((y % 4 === 0 && y % 100 !== 0) || y % 400 === 0))
|
|
90
|
+
return 29;
|
|
91
|
+
return table[m] ?? 30;
|
|
92
|
+
}
|
|
93
|
+
function dateToEpochDays(d) {
|
|
94
|
+
const y = d.year - 1;
|
|
95
|
+
let days = 365 * y + Math.floor(y / 4) - Math.floor(y / 100) + Math.floor(y / 400);
|
|
96
|
+
for (let m = 1; m < d.month; m++)
|
|
97
|
+
days += DAYS_IN_MONTH_TABLE(d.year, m);
|
|
98
|
+
days += d.day;
|
|
99
|
+
return days - 719163;
|
|
100
|
+
}
|
|
101
|
+
function epochDaysToDate(days) {
|
|
102
|
+
let remaining = days + 719162;
|
|
103
|
+
const year400 = Math.floor(remaining / 146097);
|
|
104
|
+
remaining %= 146097;
|
|
105
|
+
const year100 = Math.min(Math.floor(remaining / 36524), 3);
|
|
106
|
+
remaining -= year100 * 36524;
|
|
107
|
+
const year4 = Math.floor(remaining / 1461);
|
|
108
|
+
remaining %= 1461;
|
|
109
|
+
const year1 = Math.min(Math.floor(remaining / 365), 3);
|
|
110
|
+
remaining -= year1 * 365;
|
|
111
|
+
const year = year400 * 400 + year100 * 100 + year4 * 4 + year1 + 1;
|
|
112
|
+
let month = 1;
|
|
113
|
+
while (month <= 12 && remaining >= DAYS_IN_MONTH_TABLE(year, month)) {
|
|
114
|
+
remaining -= DAYS_IN_MONTH_TABLE(year, month);
|
|
115
|
+
month++;
|
|
116
|
+
}
|
|
117
|
+
return { type: "date", year, month, day: remaining + 1 };
|
|
118
|
+
}
|
|
119
|
+
function dateTimeToEpochSeconds(dt) {
|
|
120
|
+
const days = dateToEpochDays(dt.date);
|
|
121
|
+
return (days * 86400 +
|
|
122
|
+
dt.time.hour * 3600 +
|
|
123
|
+
dt.time.minute * 60 +
|
|
124
|
+
dt.time.second -
|
|
125
|
+
(dt.time.offsetSeconds ?? 0));
|
|
126
|
+
}
|
|
127
|
+
function epochSecondsToDateTime(secs, offsetSeconds, timezone) {
|
|
128
|
+
const localSecs = secs + (offsetSeconds ?? 0);
|
|
129
|
+
const days = Math.floor(localSecs / 86400);
|
|
130
|
+
const rem = ((localSecs % 86400) + 86400) % 86400;
|
|
131
|
+
const date = epochDaysToDate(days);
|
|
132
|
+
const time = {
|
|
133
|
+
type: "time",
|
|
134
|
+
hour: Math.floor(rem / 3600),
|
|
135
|
+
minute: Math.floor((rem % 3600) / 60),
|
|
136
|
+
second: rem % 60,
|
|
137
|
+
offsetSeconds,
|
|
138
|
+
timezone,
|
|
139
|
+
};
|
|
140
|
+
return { type: "date-time", date, time };
|
|
141
|
+
}
|
|
142
|
+
// -------------------------------------------------------------------------
|
|
143
|
+
// Core evaluator
|
|
144
|
+
// -------------------------------------------------------------------------
|
|
145
|
+
export function evaluate(node, ctx) {
|
|
146
|
+
switch (node.kind) {
|
|
147
|
+
case "null":
|
|
148
|
+
return null;
|
|
149
|
+
case "boolean":
|
|
150
|
+
return node.value;
|
|
151
|
+
case "number":
|
|
152
|
+
return node.value;
|
|
153
|
+
case "string":
|
|
154
|
+
return node.value;
|
|
155
|
+
case "temporal":
|
|
156
|
+
return parseTemporal(node.raw);
|
|
157
|
+
case "name": {
|
|
158
|
+
if (node.name === "?")
|
|
159
|
+
return ctx.input ?? null;
|
|
160
|
+
// Check built-in
|
|
161
|
+
const builtin = getBuiltin(node.name);
|
|
162
|
+
if (builtin)
|
|
163
|
+
return builtin;
|
|
164
|
+
return lookupVar(ctx, node.name);
|
|
165
|
+
}
|
|
166
|
+
case "unary-minus": {
|
|
167
|
+
const val = evaluate(node.operand, ctx);
|
|
168
|
+
if (typeof val === "number")
|
|
169
|
+
return -val;
|
|
170
|
+
if (isFeelDayTimeDuration(val))
|
|
171
|
+
return { type: "days-time-duration", seconds: -val.seconds };
|
|
172
|
+
if (isFeelYearsMonthsDuration(val))
|
|
173
|
+
return { type: "years-months-duration", months: -val.months };
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
case "binary":
|
|
177
|
+
return evalBinary(node.op, node.left, node.right, ctx);
|
|
178
|
+
case "list":
|
|
179
|
+
return node.items.map((item) => evaluate(item, ctx));
|
|
180
|
+
case "context": {
|
|
181
|
+
const result = {};
|
|
182
|
+
for (const entry of node.entries) {
|
|
183
|
+
result[entry.key] = evaluate(entry.value, ctx);
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
case "range": {
|
|
188
|
+
const rangeStart = evaluate(node.low, ctx);
|
|
189
|
+
const rangeEnd = evaluate(node.high, ctx);
|
|
190
|
+
return {
|
|
191
|
+
type: "range",
|
|
192
|
+
start: rangeStart,
|
|
193
|
+
startIncluded: node.startIncluded,
|
|
194
|
+
end: rangeEnd,
|
|
195
|
+
endIncluded: node.endIncluded,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
case "path": {
|
|
199
|
+
const base = evaluate(node.base, ctx);
|
|
200
|
+
if (base === null)
|
|
201
|
+
return null;
|
|
202
|
+
// Path on a list maps over elements
|
|
203
|
+
if (isFeelList(base)) {
|
|
204
|
+
return base.map((item) => getProperty(item, node.key));
|
|
205
|
+
}
|
|
206
|
+
return getProperty(base, node.key);
|
|
207
|
+
}
|
|
208
|
+
case "filter": {
|
|
209
|
+
const base = evaluate(node.base, ctx);
|
|
210
|
+
if (!isFeelList(base)) {
|
|
211
|
+
if (base === null)
|
|
212
|
+
return [];
|
|
213
|
+
// single value
|
|
214
|
+
const result = evaluate(node.condition, childCtx(ctx, { item: base }));
|
|
215
|
+
return typeof result === "number" ? [base] : result ? [base] : [];
|
|
216
|
+
}
|
|
217
|
+
// Numeric index filter
|
|
218
|
+
const first = evaluate(node.condition, childCtx(ctx, { item: base[0] ?? null }));
|
|
219
|
+
if (typeof first === "number") {
|
|
220
|
+
const idx = first > 0 ? first - 1 : base.length + first;
|
|
221
|
+
const val = base[Math.floor(idx)];
|
|
222
|
+
return val !== undefined ? val : null;
|
|
223
|
+
}
|
|
224
|
+
return base.filter((item) => {
|
|
225
|
+
const r = evaluate(node.condition, childCtx(ctx, { item }));
|
|
226
|
+
return r === true || (r !== false && r !== null);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
case "call":
|
|
230
|
+
return evalCall(node.callee, node.args, ctx);
|
|
231
|
+
case "call-named": {
|
|
232
|
+
const builtin = getBuiltin(node.callee);
|
|
233
|
+
if (!builtin) {
|
|
234
|
+
const fn = lookupVar(ctx, node.callee);
|
|
235
|
+
if (fn === null || typeof fn !== "object" || !("call" in fn))
|
|
236
|
+
return null;
|
|
237
|
+
const args = node.args.map((a) => evaluate(a.value, ctx));
|
|
238
|
+
return fn.call(args);
|
|
239
|
+
}
|
|
240
|
+
// Map named args to positional (built-ins don't declare param names in registry)
|
|
241
|
+
const args = node.args.map((a) => evaluate(a.value, ctx));
|
|
242
|
+
return builtin.call(args);
|
|
243
|
+
}
|
|
244
|
+
case "if": {
|
|
245
|
+
const cond = evaluate(node.condition, ctx);
|
|
246
|
+
return cond === true ? evaluate(node.then, ctx) : evaluate(node.else, ctx);
|
|
247
|
+
}
|
|
248
|
+
case "for": {
|
|
249
|
+
const domains = [];
|
|
250
|
+
for (const binding of node.bindings) {
|
|
251
|
+
const d = evaluate(binding.domain, ctx);
|
|
252
|
+
domains.push(isFeelList(d) ? d : [d]);
|
|
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
|
+
}
|
|
274
|
+
case "between": {
|
|
275
|
+
const val = evaluate(node.value, ctx);
|
|
276
|
+
const low = evaluate(node.low, ctx);
|
|
277
|
+
const high = evaluate(node.high, ctx);
|
|
278
|
+
const cmpLow = compareValues(val, low);
|
|
279
|
+
const cmpHigh = compareValues(val, high);
|
|
280
|
+
if (cmpLow === null || cmpHigh === null)
|
|
281
|
+
return null;
|
|
282
|
+
return cmpLow >= 0 && cmpHigh <= 0;
|
|
283
|
+
}
|
|
284
|
+
case "in-test": {
|
|
285
|
+
const val = evaluate(node.value, ctx);
|
|
286
|
+
const test = evaluate(node.test, ctx);
|
|
287
|
+
return testIncludes(test, val);
|
|
288
|
+
}
|
|
289
|
+
case "instance-of": {
|
|
290
|
+
const val = evaluate(node.value, ctx);
|
|
291
|
+
return checkInstanceOf(val, node.typeName);
|
|
292
|
+
}
|
|
293
|
+
case "function-def":
|
|
294
|
+
return {
|
|
295
|
+
type: "function",
|
|
296
|
+
paramNames: node.params,
|
|
297
|
+
call: (args) => {
|
|
298
|
+
const vars = {};
|
|
299
|
+
for (let i = 0; i < node.params.length; i++) {
|
|
300
|
+
vars[node.params[i] ?? ""] = args[i] ?? null;
|
|
301
|
+
}
|
|
302
|
+
return evaluate(node.body, childCtx(ctx, vars));
|
|
303
|
+
},
|
|
304
|
+
};
|
|
305
|
+
case "any-input":
|
|
306
|
+
return true;
|
|
307
|
+
case "unary-test-list": {
|
|
308
|
+
const input = ctx.input ?? null;
|
|
309
|
+
for (const test of node.tests) {
|
|
310
|
+
const r = evaluateUnaryTest(test, input, ctx);
|
|
311
|
+
if (r)
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
}
|
|
316
|
+
case "unary-not": {
|
|
317
|
+
const input = ctx.input ?? null;
|
|
318
|
+
for (const test of node.tests) {
|
|
319
|
+
const r = evaluateUnaryTest(test, input, ctx);
|
|
320
|
+
if (r)
|
|
321
|
+
return false;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
function evalBinary(op, leftNode, rightNode, ctx) {
|
|
328
|
+
// Short-circuit for and/or
|
|
329
|
+
if (op === "and") {
|
|
330
|
+
const l = evaluate(leftNode, ctx);
|
|
331
|
+
if (l === false)
|
|
332
|
+
return false;
|
|
333
|
+
const r = evaluate(rightNode, ctx);
|
|
334
|
+
if (r === false)
|
|
335
|
+
return false;
|
|
336
|
+
if (l === null || r === null)
|
|
337
|
+
return null;
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
if (op === "or") {
|
|
341
|
+
const l = evaluate(leftNode, ctx);
|
|
342
|
+
if (l === true)
|
|
343
|
+
return true;
|
|
344
|
+
const r = evaluate(rightNode, ctx);
|
|
345
|
+
if (r === true)
|
|
346
|
+
return true;
|
|
347
|
+
if (l === null || r === null)
|
|
348
|
+
return null;
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
351
|
+
const left = evaluate(leftNode, ctx);
|
|
352
|
+
const right = evaluate(rightNode, ctx);
|
|
353
|
+
if (op === "=")
|
|
354
|
+
return deepEqual(left, right);
|
|
355
|
+
if (op === "!=")
|
|
356
|
+
return !deepEqual(left, right);
|
|
357
|
+
if (left === null || right === null)
|
|
358
|
+
return null;
|
|
359
|
+
if (op === "+" || op === "-") {
|
|
360
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
361
|
+
return op === "+" ? left + right : left - right;
|
|
362
|
+
}
|
|
363
|
+
if (typeof left === "string" && typeof right === "string" && op === "+") {
|
|
364
|
+
return left + right;
|
|
365
|
+
}
|
|
366
|
+
if (op === "+")
|
|
367
|
+
return addDuration(left, right);
|
|
368
|
+
return subtractValues(left, right);
|
|
369
|
+
}
|
|
370
|
+
if (op === "*") {
|
|
371
|
+
if (typeof left === "number" && typeof right === "number")
|
|
372
|
+
return left * right;
|
|
373
|
+
if (typeof left === "number" && isFeelDayTimeDuration(right))
|
|
374
|
+
return { type: "days-time-duration", seconds: left * right.seconds };
|
|
375
|
+
if (typeof left === "number" && isFeelYearsMonthsDuration(right))
|
|
376
|
+
return { type: "years-months-duration", months: left * right.months };
|
|
377
|
+
if (isFeelDayTimeDuration(left) && typeof right === "number")
|
|
378
|
+
return { type: "days-time-duration", seconds: left.seconds * right };
|
|
379
|
+
if (isFeelYearsMonthsDuration(left) && typeof right === "number")
|
|
380
|
+
return { type: "years-months-duration", months: left.months * right };
|
|
381
|
+
return null;
|
|
382
|
+
}
|
|
383
|
+
if (op === "/") {
|
|
384
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
385
|
+
return right === 0 ? null : left / right;
|
|
386
|
+
}
|
|
387
|
+
if (isFeelDayTimeDuration(left) && typeof right === "number")
|
|
388
|
+
return { type: "days-time-duration", seconds: left.seconds / right };
|
|
389
|
+
if (isFeelYearsMonthsDuration(left) && typeof right === "number")
|
|
390
|
+
return { type: "years-months-duration", months: left.months / right };
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
if (op === "**") {
|
|
394
|
+
if (typeof left === "number" && typeof right === "number")
|
|
395
|
+
return left ** right;
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
// Comparison
|
|
399
|
+
const cmp = compareValues(left, right);
|
|
400
|
+
if (cmp === null)
|
|
401
|
+
return null;
|
|
402
|
+
if (op === "<")
|
|
403
|
+
return cmp < 0;
|
|
404
|
+
if (op === "<=")
|
|
405
|
+
return cmp <= 0;
|
|
406
|
+
if (op === ">")
|
|
407
|
+
return cmp > 0;
|
|
408
|
+
if (op === ">=")
|
|
409
|
+
return cmp >= 0;
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
function evalCall(callee, argNodes, ctx) {
|
|
413
|
+
const builtin = getBuiltin(callee);
|
|
414
|
+
if (builtin) {
|
|
415
|
+
const args = argNodes.map((a) => evaluate(a, ctx));
|
|
416
|
+
return builtin.call(args);
|
|
417
|
+
}
|
|
418
|
+
const fn = lookupVar(ctx, callee);
|
|
419
|
+
if (fn === null || typeof fn !== "object" || !("call" in fn))
|
|
420
|
+
return null;
|
|
421
|
+
const args = argNodes.map((a) => evaluate(a, ctx));
|
|
422
|
+
return fn.call(args);
|
|
423
|
+
}
|
|
424
|
+
function evalCartesian(bindings, domains, idx, ctx, forNode, partial) {
|
|
425
|
+
if (idx === bindings.length) {
|
|
426
|
+
const vars = { partial: [...partial] };
|
|
427
|
+
const binding = bindings[idx - 1];
|
|
428
|
+
if (binding)
|
|
429
|
+
vars[binding.name] = partial[partial.length - 1] ?? null;
|
|
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] ?? [];
|
|
438
|
+
const results = [];
|
|
439
|
+
for (const val of domain) {
|
|
440
|
+
const vars = { partial: [...partial] };
|
|
441
|
+
vars[binding.name] = val;
|
|
442
|
+
// Propagate earlier bindings too
|
|
443
|
+
for (let i = 0; i < idx; i++) {
|
|
444
|
+
const b = bindings[i];
|
|
445
|
+
if (b)
|
|
446
|
+
vars[b.name] = partial[i] ?? null;
|
|
447
|
+
}
|
|
448
|
+
const sub = evalCartesian(bindings, domains, idx + 1, childCtx(ctx, vars), forNode, [
|
|
449
|
+
...partial,
|
|
450
|
+
val,
|
|
451
|
+
]);
|
|
452
|
+
results.push(...sub);
|
|
453
|
+
}
|
|
454
|
+
return results;
|
|
455
|
+
}
|
|
456
|
+
function evalQuantifier(kind, bindings, domains, idx, ctx, satisfies) {
|
|
457
|
+
if (idx === bindings.length) {
|
|
458
|
+
return evaluate(satisfies, ctx);
|
|
459
|
+
}
|
|
460
|
+
const binding = bindings[idx];
|
|
461
|
+
if (!binding)
|
|
462
|
+
return kind === "every";
|
|
463
|
+
const domain = domains[idx] ?? [];
|
|
464
|
+
if (domain.length === 0)
|
|
465
|
+
return kind === "every";
|
|
466
|
+
let hasNull = false;
|
|
467
|
+
for (const val of domain) {
|
|
468
|
+
const vars = {};
|
|
469
|
+
vars[binding.name] = val;
|
|
470
|
+
const r = evalQuantifier(kind, bindings, domains, idx + 1, childCtx(ctx, vars), satisfies);
|
|
471
|
+
if (kind === "some" && r === true)
|
|
472
|
+
return true;
|
|
473
|
+
if (kind === "every" && r === false)
|
|
474
|
+
return false;
|
|
475
|
+
if (r === null)
|
|
476
|
+
hasNull = true;
|
|
477
|
+
}
|
|
478
|
+
if (kind === "some")
|
|
479
|
+
return hasNull ? null : false;
|
|
480
|
+
return hasNull ? null : true;
|
|
481
|
+
}
|
|
482
|
+
function deepEqual(a, b) {
|
|
483
|
+
if (a === b)
|
|
484
|
+
return true;
|
|
485
|
+
if (a === null || b === null)
|
|
486
|
+
return false;
|
|
487
|
+
if (typeof a !== typeof b)
|
|
488
|
+
return false;
|
|
489
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
490
|
+
if (a.length !== b.length)
|
|
491
|
+
return false;
|
|
492
|
+
for (let i = 0; i < a.length; i++) {
|
|
493
|
+
if (!deepEqual(a[i] ?? null, b[i] ?? null))
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
499
|
+
const aKeys = Object.keys(a);
|
|
500
|
+
const bKeys = Object.keys(b);
|
|
501
|
+
if (aKeys.length !== bKeys.length)
|
|
502
|
+
return false;
|
|
503
|
+
for (const k of aKeys) {
|
|
504
|
+
const av = a[k] ?? null;
|
|
505
|
+
const bv = b[k] ?? null;
|
|
506
|
+
if (!deepEqual(av, bv))
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
return true;
|
|
510
|
+
}
|
|
511
|
+
return false;
|
|
512
|
+
}
|
|
513
|
+
function testIncludes(test, val) {
|
|
514
|
+
if (isFeelRange(test)) {
|
|
515
|
+
const cmpStart = compareValues(val, test.start);
|
|
516
|
+
const cmpEnd = compareValues(val, test.end);
|
|
517
|
+
if (cmpStart === null || cmpEnd === null)
|
|
518
|
+
return null;
|
|
519
|
+
const startOk = test.startIncluded ? cmpStart >= 0 : cmpStart > 0;
|
|
520
|
+
const endOk = test.endIncluded ? cmpEnd <= 0 : cmpEnd < 0;
|
|
521
|
+
return startOk && endOk;
|
|
522
|
+
}
|
|
523
|
+
if (isFeelList(test)) {
|
|
524
|
+
for (const t of test) {
|
|
525
|
+
const r = testIncludes(t, val);
|
|
526
|
+
if (r === true)
|
|
527
|
+
return true;
|
|
528
|
+
}
|
|
529
|
+
return false;
|
|
530
|
+
}
|
|
531
|
+
return deepEqual(val, test);
|
|
532
|
+
}
|
|
533
|
+
function checkInstanceOf(val, typeName) {
|
|
534
|
+
switch (typeName) {
|
|
535
|
+
case "number":
|
|
536
|
+
return typeof val === "number";
|
|
537
|
+
case "string":
|
|
538
|
+
return typeof val === "string";
|
|
539
|
+
case "boolean":
|
|
540
|
+
return typeof val === "boolean";
|
|
541
|
+
case "date":
|
|
542
|
+
return isFeelDate(val);
|
|
543
|
+
case "time":
|
|
544
|
+
return isFeelTime(val);
|
|
545
|
+
case "date and time":
|
|
546
|
+
return isFeelDateTime(val);
|
|
547
|
+
case "days and time duration":
|
|
548
|
+
return isFeelDayTimeDuration(val);
|
|
549
|
+
case "years and months duration":
|
|
550
|
+
return isFeelYearsMonthsDuration(val);
|
|
551
|
+
case "list":
|
|
552
|
+
return Array.isArray(val);
|
|
553
|
+
case "context":
|
|
554
|
+
return isFeelContext(val);
|
|
555
|
+
case "function":
|
|
556
|
+
return typeof val === "object" && val !== null && "call" in val;
|
|
557
|
+
case "Any":
|
|
558
|
+
return true;
|
|
559
|
+
case "null":
|
|
560
|
+
return val === null;
|
|
561
|
+
default:
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
/** Evaluate a unary test against an input value. Returns boolean. */
|
|
566
|
+
export function evaluateUnaryTest(node, input, ctx) {
|
|
567
|
+
const withInput = { ...ctx, input };
|
|
568
|
+
const result = evaluate(node, withInput);
|
|
569
|
+
if (typeof result === "boolean")
|
|
570
|
+
return result;
|
|
571
|
+
// Range result in unary-test context → membership test
|
|
572
|
+
if (isFeelRange(result))
|
|
573
|
+
return testIncludes(result, input) === true;
|
|
574
|
+
// List result → any element matches
|
|
575
|
+
if (isFeelList(result))
|
|
576
|
+
return result.some((r) => testIncludes(r, input) === true);
|
|
577
|
+
// A plain expression in unary test mode is an equality test
|
|
578
|
+
if (result !== null && result !== undefined)
|
|
579
|
+
return deepEqual(result, input);
|
|
580
|
+
return false;
|
|
581
|
+
}
|
|
582
|
+
/** Evaluate a full unary-test node (the root returned by parseUnaryTests). */
|
|
583
|
+
export function evaluateUnaryTests(node, input, ctx) {
|
|
584
|
+
return evaluateUnaryTest(node, input, ctx);
|
|
585
|
+
}
|
|
586
|
+
//# sourceMappingURL=evaluator.js.map
|