odant-work 1.0.12 → 1.0.13
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/#system/certs/russian_trusted_root_ca.pem +33 -0
- package/#system/mail.json.example +9 -0
- package/#system/package-manifest.json +986 -0
- package/#system/sandbox.json +19 -0
- package/#system/settings.json.example +3 -0
- package/package.json +1 -1
- package/run.mjs +78 -11
- package/scripts/check-ignored-code.mjs +25 -1
- package/sources/data/api.js +1071 -0
- package/sources/data/choices.js +125 -0
- package/sources/data/classes.js +412 -0
- package/sources/data/db.js +383 -0
- package/sources/data/errors.js +15 -0
- package/sources/data/expr.js +456 -0
- package/sources/data/objects.js +950 -0
- package/sources/data/posting.js +657 -0
- package/sources/data/print.js +524 -0
- package/sources/data/progress.md +526 -0
- package/sources/data/readme.md +77 -0
- package/sources/data/reports.js +1033 -0
- package/sources/data/schema.js +586 -0
- package/sources/data/scope.js +281 -0
- package/sources/data/sql-inspector.js +892 -0
- package/sources/data/time.js +67 -0
- package/sources/data/workspace.js +152 -0
- package/sources/host/auto-updater.js +52 -10
- package/.vscode/launch.json +0 -32
- package/.vscode/settings.json +0 -2
- package/.vscode/tasks.json +0 -65
- package/ORGANIZATIONS//320/221/320/270/320/267/320/275/320/265/321/201 /320/230/320/275/321/202/320/265/321/200 /320/241/320/276/321/204/321/202/orders/$structure/video/IMG_8524.MP4 +0 -0
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Мини-язык выражений правил проводок и вычисляемых полей.
|
|
3
|
+
*
|
|
4
|
+
* Не JS: разбор и вычисление собственные, доступа к глобалам, прототипам и
|
|
5
|
+
* произвольному коду нет. Выражение можно хранить в class.js, править в форме
|
|
6
|
+
* и генерировать ИИ без риска исполнения постороннего кода.
|
|
7
|
+
*
|
|
8
|
+
* Синтаксис:
|
|
9
|
+
* числа 12.5, строки 'а' "б", true false null
|
|
10
|
+
* имена полей: sum, client, row.sum, row['поле с пробелом']
|
|
11
|
+
* операторы: + - * / % == != < <= > >= && || ! a ? b : c ( )
|
|
12
|
+
* функции: round(x, n) min(...) max(...) abs(x) sum(...) count(x) avg(...)
|
|
13
|
+
* num(x) coalesce(a, b, ...) if(c, a, b) + функции вызывающего (norm, coef, ...)
|
|
14
|
+
*
|
|
15
|
+
* Семантика:
|
|
16
|
+
* - отсутствующее поле — undefined; доступ к полю у пустого значения — undefined;
|
|
17
|
+
* - доступ к полю массива — массив значений поля (staff.sum → [строка1.sum, ...]),
|
|
18
|
+
* поэтому sum(staff.sum) — итог табличной части;
|
|
19
|
+
* - в арифметике пустое значение (undefined, null, '') — 0, числовая строка — число;
|
|
20
|
+
* - `+` складывает числа и склеивает строки, если хотя бы одна сторона — нечисловая строка;
|
|
21
|
+
* - деление на ноль и нечисловой результат — ошибка (ExprError).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export class ExprError extends Error {
|
|
25
|
+
constructor(message, src, pos) {
|
|
26
|
+
super(`выражение «${src}»${pos != null ? ` (позиция ${pos + 1})` : ''}: ${message}`);
|
|
27
|
+
this.name = 'ExprError';
|
|
28
|
+
this.pos = pos;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MAX_LENGTH = 2000;
|
|
33
|
+
const MAX_DEPTH = 64;
|
|
34
|
+
const FORBIDDEN = new Set(['__proto__', 'prototype', 'constructor']);
|
|
35
|
+
const LITERALS = { true: true, false: false, null: null };
|
|
36
|
+
const OPERATORS = ['==', '!=', '<=', '>=', '&&', '||', '!', '<', '>', '+', '-', '*', '/', '%', '?', ':', '(', ')', ',', '.', '[', ']'];
|
|
37
|
+
|
|
38
|
+
/* ---------- лексер ---------- */
|
|
39
|
+
|
|
40
|
+
function tokenize(src) {
|
|
41
|
+
const out = [];
|
|
42
|
+
let i = 0;
|
|
43
|
+
while (i < src.length) {
|
|
44
|
+
const ch = src[i];
|
|
45
|
+
if (/\s/.test(ch)) {
|
|
46
|
+
i++;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const num = /^\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/.exec(src.slice(i));
|
|
50
|
+
if (num) {
|
|
51
|
+
out.push({ t: 'num', v: Number(num[0]), pos: i });
|
|
52
|
+
i += num[0].length;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === '"' || ch === "'") {
|
|
56
|
+
let j = i + 1, s = '';
|
|
57
|
+
while (j < src.length && src[j] !== ch) {
|
|
58
|
+
if (src[j] === '\\' && j + 1 < src.length) {
|
|
59
|
+
const e = src[j + 1];
|
|
60
|
+
s += e === 'n' ? '\n' : e === 't' ? '\t' : e;
|
|
61
|
+
j += 2;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
s += src[j++];
|
|
65
|
+
}
|
|
66
|
+
if (j >= src.length)
|
|
67
|
+
throw new ExprError('незакрытая строка', src, i);
|
|
68
|
+
out.push({ t: 'str', v: s, pos: i });
|
|
69
|
+
i = j + 1;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
const id = /^[\p{L}_$][\p{L}\p{N}_$]*/u.exec(src.slice(i));
|
|
73
|
+
if (id) {
|
|
74
|
+
const name = id[0];
|
|
75
|
+
if (Object.hasOwn(LITERALS, name))
|
|
76
|
+
out.push({ t: 'lit', v: LITERALS[name], pos: i });
|
|
77
|
+
else
|
|
78
|
+
out.push({ t: 'id', v: name, pos: i });
|
|
79
|
+
i += name.length;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const op = OPERATORS.find(o => src.startsWith(o, i));
|
|
83
|
+
if (!op)
|
|
84
|
+
throw new ExprError(`недопустимый символ «${ch}»`, src, i);
|
|
85
|
+
out.push({ t: 'op', v: op, pos: i });
|
|
86
|
+
i += op.length;
|
|
87
|
+
}
|
|
88
|
+
out.push({ t: 'end', pos: src.length });
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* ---------- парсер (рекурсивный спуск по приоритетам) ---------- */
|
|
93
|
+
|
|
94
|
+
const BINARY = [
|
|
95
|
+
['||'],
|
|
96
|
+
['&&'],
|
|
97
|
+
['==', '!='],
|
|
98
|
+
['<', '<=', '>', '>='],
|
|
99
|
+
['+', '-'],
|
|
100
|
+
['*', '/', '%'],
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
function parse(src) {
|
|
104
|
+
const tokens = tokenize(src);
|
|
105
|
+
let k = 0, depth = 0;
|
|
106
|
+
const peek = () => tokens[k];
|
|
107
|
+
const isOp = v => peek().t === 'op' && peek().v === v;
|
|
108
|
+
const expect = v => {
|
|
109
|
+
if (!isOp(v))
|
|
110
|
+
throw new ExprError(`ожидалось «${v}»`, src, peek().pos);
|
|
111
|
+
k++;
|
|
112
|
+
};
|
|
113
|
+
const enter = () => {
|
|
114
|
+
if (++depth > MAX_DEPTH)
|
|
115
|
+
throw new ExprError('слишком глубокая вложенность', src, peek().pos);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
function ternary() {
|
|
119
|
+
enter();
|
|
120
|
+
const c = binary(0);
|
|
121
|
+
let node = c;
|
|
122
|
+
if (isOp('?')) {
|
|
123
|
+
k++;
|
|
124
|
+
const a = ternary();
|
|
125
|
+
expect(':');
|
|
126
|
+
const b = ternary();
|
|
127
|
+
node = { t: 'cond', c, a, b };
|
|
128
|
+
}
|
|
129
|
+
depth--;
|
|
130
|
+
return node;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function binary(level) {
|
|
134
|
+
if (level >= BINARY.length)
|
|
135
|
+
return unary();
|
|
136
|
+
let a = binary(level + 1);
|
|
137
|
+
while (peek().t === 'op' && BINARY[level].includes(peek().v)) {
|
|
138
|
+
const op = tokens[k++].v;
|
|
139
|
+
const b = binary(level + 1);
|
|
140
|
+
a = { t: 'bin', op, a, b };
|
|
141
|
+
}
|
|
142
|
+
return a;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function unary() {
|
|
146
|
+
if (isOp('!') || isOp('-') || isOp('+')) {
|
|
147
|
+
const op = tokens[k++].v;
|
|
148
|
+
enter();
|
|
149
|
+
const a = unary();
|
|
150
|
+
depth--;
|
|
151
|
+
return { t: 'un', op, a };
|
|
152
|
+
}
|
|
153
|
+
return postfix();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function postfix() {
|
|
157
|
+
let node = primary();
|
|
158
|
+
for (;;) {
|
|
159
|
+
if (isOp('.')) {
|
|
160
|
+
k++;
|
|
161
|
+
const t = peek();
|
|
162
|
+
if (t.t !== 'id' && t.t !== 'lit')
|
|
163
|
+
throw new ExprError('ожидалось имя поля после «.»', src, t.pos);
|
|
164
|
+
k++;
|
|
165
|
+
// имя поля может совпадать с литералом (row.null): String(null) === 'null'
|
|
166
|
+
node = { t: 'mem', obj: node, key: String(t.v) };
|
|
167
|
+
}
|
|
168
|
+
else if (isOp('[')) {
|
|
169
|
+
k++;
|
|
170
|
+
const key = ternary();
|
|
171
|
+
expect(']');
|
|
172
|
+
node = { t: 'idx', obj: node, key };
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
return node;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function primary() {
|
|
180
|
+
const t = peek();
|
|
181
|
+
if (t.t === 'num' || t.t === 'str' || t.t === 'lit') {
|
|
182
|
+
k++;
|
|
183
|
+
return { t: 'val', v: t.v };
|
|
184
|
+
}
|
|
185
|
+
if (t.t === 'id') {
|
|
186
|
+
k++;
|
|
187
|
+
if (isOp('(')) {
|
|
188
|
+
k++;
|
|
189
|
+
const args = [];
|
|
190
|
+
if (!isOp(')')) {
|
|
191
|
+
args.push(ternary());
|
|
192
|
+
while (isOp(',')) {
|
|
193
|
+
k++;
|
|
194
|
+
args.push(ternary());
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
expect(')');
|
|
198
|
+
return { t: 'call', name: t.v, args, pos: t.pos };
|
|
199
|
+
}
|
|
200
|
+
return { t: 'id', name: t.v };
|
|
201
|
+
}
|
|
202
|
+
if (isOp('(')) {
|
|
203
|
+
k++;
|
|
204
|
+
const e = ternary();
|
|
205
|
+
expect(')');
|
|
206
|
+
return e;
|
|
207
|
+
}
|
|
208
|
+
throw new ExprError(t.t === 'end' ? 'неожиданный конец' : 'ожидалось значение', src, t.pos);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ast = ternary();
|
|
212
|
+
if (peek().t !== 'end')
|
|
213
|
+
throw new ExprError('лишний текст', src, peek().pos);
|
|
214
|
+
return ast;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/* ---------- значения ---------- */
|
|
218
|
+
|
|
219
|
+
const isEmpty = v => v === undefined || v === null || v === '';
|
|
220
|
+
|
|
221
|
+
function isNumeric(v) {
|
|
222
|
+
if (typeof v === 'number')
|
|
223
|
+
return true;
|
|
224
|
+
return typeof v === 'string' && v.trim() !== '' && Number.isFinite(Number(v));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Число из значения: пусто → 0, числовая строка → число, иначе ошибка. */
|
|
228
|
+
export function toNumber(v, fail = msg => { throw new Error(msg); }) {
|
|
229
|
+
if (isEmpty(v))
|
|
230
|
+
return 0;
|
|
231
|
+
if (typeof v === 'number')
|
|
232
|
+
return v;
|
|
233
|
+
if (typeof v === 'boolean')
|
|
234
|
+
return v ? 1 : 0;
|
|
235
|
+
if (isNumeric(v))
|
|
236
|
+
return Number(v);
|
|
237
|
+
return fail(`«${typeof v === 'object' ? JSON.stringify(v) : v}» — не число`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/** Коммерческое округление (половина — от нуля) без ошибок двоичной дроби. */
|
|
241
|
+
export function round(x, n = 0) {
|
|
242
|
+
const digits = Math.max(0, Math.min(10, Math.trunc(n)));
|
|
243
|
+
const sign = x < 0 ? -1 : 1;
|
|
244
|
+
const r = Number(Math.round(Number(Math.abs(x) + 'e' + digits)) + 'e-' + digits);
|
|
245
|
+
return sign * r || 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function member(obj, key) {
|
|
249
|
+
if (FORBIDDEN.has(key) || obj === undefined || obj === null)
|
|
250
|
+
return undefined;
|
|
251
|
+
if (Array.isArray(obj)) {
|
|
252
|
+
if (key === 'length')
|
|
253
|
+
return obj.length;
|
|
254
|
+
return obj.map(item => member(item, key));
|
|
255
|
+
}
|
|
256
|
+
if (typeof obj === 'object' && Object.hasOwn(obj, key))
|
|
257
|
+
return obj[key];
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function flat(args) {
|
|
262
|
+
return args.flat(Infinity).filter(v => !isEmpty(v));
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* ---------- встроенные функции ---------- */
|
|
266
|
+
|
|
267
|
+
const BUILTINS = {
|
|
268
|
+
round: (num, x, n) => round(num(x), num(n)),
|
|
269
|
+
abs: (num, x) => Math.abs(num(x)),
|
|
270
|
+
num: (num, x) => num(x),
|
|
271
|
+
min: (num, ...a) => {
|
|
272
|
+
const v = flat(a).map(num);
|
|
273
|
+
return v.length ? Math.min(...v) : 0;
|
|
274
|
+
},
|
|
275
|
+
max: (num, ...a) => {
|
|
276
|
+
const v = flat(a).map(num);
|
|
277
|
+
return v.length ? Math.max(...v) : 0;
|
|
278
|
+
},
|
|
279
|
+
sum: (num, ...a) => flat(a).reduce((s, v) => s + num(v), 0),
|
|
280
|
+
count: (_num, ...a) => flat(a).length,
|
|
281
|
+
avg: (num, ...a) => {
|
|
282
|
+
const v = flat(a).map(num);
|
|
283
|
+
return v.length ? v.reduce((s, x) => s + x, 0) / v.length : 0;
|
|
284
|
+
},
|
|
285
|
+
coalesce: (_num, ...a) => a.find(v => !isEmpty(v)),
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
/* ---------- вычисление ---------- */
|
|
289
|
+
|
|
290
|
+
function evaluate(node, scope, fns, src) {
|
|
291
|
+
const fail = msg => { throw new ExprError(msg, src); };
|
|
292
|
+
const num = v => toNumber(v, fail);
|
|
293
|
+
const ev = n => evaluate(n, scope, fns, src);
|
|
294
|
+
switch (node.t) {
|
|
295
|
+
case 'val':
|
|
296
|
+
return node.v;
|
|
297
|
+
case 'id':
|
|
298
|
+
return Object.hasOwn(scope, node.name) ? scope[node.name] : undefined;
|
|
299
|
+
case 'mem':
|
|
300
|
+
return member(ev(node.obj), node.key);
|
|
301
|
+
case 'idx': {
|
|
302
|
+
const obj = ev(node.obj);
|
|
303
|
+
const key = ev(node.key);
|
|
304
|
+
if (Array.isArray(obj) && typeof key === 'number')
|
|
305
|
+
return obj[key];
|
|
306
|
+
return member(obj, String(key));
|
|
307
|
+
}
|
|
308
|
+
case 'un': {
|
|
309
|
+
const a = ev(node.a);
|
|
310
|
+
if (node.op === '!')
|
|
311
|
+
return !a;
|
|
312
|
+
return node.op === '-' ? -num(a) : num(a);
|
|
313
|
+
}
|
|
314
|
+
case 'cond':
|
|
315
|
+
return ev(node.c) ? ev(node.a) : ev(node.b);
|
|
316
|
+
case 'bin': {
|
|
317
|
+
if (node.op === '&&')
|
|
318
|
+
return ev(node.a) && ev(node.b);
|
|
319
|
+
if (node.op === '||')
|
|
320
|
+
return ev(node.a) || ev(node.b);
|
|
321
|
+
const a = ev(node.a), b = ev(node.b);
|
|
322
|
+
switch (node.op) {
|
|
323
|
+
case '+':
|
|
324
|
+
if ((typeof a === 'string' && !isNumeric(a)) || (typeof b === 'string' && !isNumeric(b)))
|
|
325
|
+
return String(a ?? '') + String(b ?? '');
|
|
326
|
+
return num(a) + num(b);
|
|
327
|
+
case '-': return num(a) - num(b);
|
|
328
|
+
case '*': return num(a) * num(b);
|
|
329
|
+
case '/':
|
|
330
|
+
case '%': {
|
|
331
|
+
const d = num(b);
|
|
332
|
+
if (d === 0)
|
|
333
|
+
fail('деление на ноль');
|
|
334
|
+
return node.op === '/' ? num(a) / d : num(a) % d;
|
|
335
|
+
}
|
|
336
|
+
case '==': return equal(a, b);
|
|
337
|
+
case '!=': return !equal(a, b);
|
|
338
|
+
default: return compare(node.op, a, b, fail);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
case 'call': {
|
|
342
|
+
if (node.name === 'if') {
|
|
343
|
+
if (node.args.length !== 3)
|
|
344
|
+
fail('if(условие, да, нет) — три аргумента');
|
|
345
|
+
return ev(node.args[0]) ? ev(node.args[1]) : ev(node.args[2]);
|
|
346
|
+
}
|
|
347
|
+
const args = node.args.map(ev);
|
|
348
|
+
if (Object.hasOwn(BUILTINS, node.name))
|
|
349
|
+
return BUILTINS[node.name](num, ...args);
|
|
350
|
+
if (fns && Object.hasOwn(fns, node.name) && typeof fns[node.name] === 'function')
|
|
351
|
+
return fns[node.name](...args);
|
|
352
|
+
return fail(`неизвестная функция ${node.name}()`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return fail('внутренняя ошибка разбора');
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function equal(a, b) {
|
|
359
|
+
if (isEmpty(a) && isEmpty(b))
|
|
360
|
+
return true;
|
|
361
|
+
if (isNumeric(a) && isNumeric(b))
|
|
362
|
+
return Number(a) === Number(b);
|
|
363
|
+
return a === b;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function compare(op, a, b, fail) {
|
|
367
|
+
let x = a, y = b;
|
|
368
|
+
if ((isNumeric(a) || isEmpty(a)) && (isNumeric(b) || isEmpty(b))) {
|
|
369
|
+
x = toNumber(a);
|
|
370
|
+
y = toNumber(b);
|
|
371
|
+
}
|
|
372
|
+
else if (typeof a !== 'string' || typeof b !== 'string')
|
|
373
|
+
fail(`нельзя сравнить «${a}» и «${b}»`);
|
|
374
|
+
switch (op) {
|
|
375
|
+
case '<': return x < y;
|
|
376
|
+
case '<=': return x <= y;
|
|
377
|
+
case '>': return x > y;
|
|
378
|
+
default: return x >= y;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/* ---------- анализ ---------- */
|
|
383
|
+
|
|
384
|
+
function collect(node, ids, calls) {
|
|
385
|
+
switch (node.t) {
|
|
386
|
+
case 'id':
|
|
387
|
+
ids.add(node.name);
|
|
388
|
+
return;
|
|
389
|
+
case 'mem':
|
|
390
|
+
return collect(node.obj, ids, calls);
|
|
391
|
+
case 'idx':
|
|
392
|
+
collect(node.obj, ids, calls);
|
|
393
|
+
return collect(node.key, ids, calls);
|
|
394
|
+
case 'un':
|
|
395
|
+
return collect(node.a, ids, calls);
|
|
396
|
+
case 'bin':
|
|
397
|
+
collect(node.a, ids, calls);
|
|
398
|
+
return collect(node.b, ids, calls);
|
|
399
|
+
case 'cond':
|
|
400
|
+
collect(node.c, ids, calls);
|
|
401
|
+
collect(node.a, ids, calls);
|
|
402
|
+
return collect(node.b, ids, calls);
|
|
403
|
+
case 'call':
|
|
404
|
+
calls.add(node.name);
|
|
405
|
+
for (const a of node.args)
|
|
406
|
+
collect(a, ids, calls);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ---------- API ---------- */
|
|
411
|
+
|
|
412
|
+
const cache = new Map();
|
|
413
|
+
const CACHE_MAX = 2000;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Разобрать выражение (с кэшем).
|
|
417
|
+
* @param {string|number} source Выражение; число — константа
|
|
418
|
+
* @returns {{ source: string, names: Set<string>, calls: Set<string>, run: (scope?: object, fns?: object) => any }}
|
|
419
|
+
* names — корневые имена (поля, row, …), calls — имена вызываемых функций
|
|
420
|
+
*/
|
|
421
|
+
export function compile(source) {
|
|
422
|
+
const src = String(source ?? '').trim();
|
|
423
|
+
const hit = cache.get(src);
|
|
424
|
+
if (hit)
|
|
425
|
+
return hit;
|
|
426
|
+
if (!src)
|
|
427
|
+
throw new ExprError('пустое выражение', src);
|
|
428
|
+
if (src.length > MAX_LENGTH)
|
|
429
|
+
throw new ExprError(`длиннее ${MAX_LENGTH} символов`, src.slice(0, 40) + '…');
|
|
430
|
+
const ast = parse(src);
|
|
431
|
+
const names = new Set(), calls = new Set();
|
|
432
|
+
collect(ast, names, calls);
|
|
433
|
+
const compiled = {
|
|
434
|
+
source: src,
|
|
435
|
+
names,
|
|
436
|
+
calls,
|
|
437
|
+
run(scope = {}, fns = {}) {
|
|
438
|
+
const v = evaluate(ast, scope, fns, src);
|
|
439
|
+
if (typeof v === 'number' && !Number.isFinite(v))
|
|
440
|
+
throw new ExprError('результат — не конечное число', src);
|
|
441
|
+
return v;
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
if (cache.size >= CACHE_MAX)
|
|
445
|
+
cache.clear();
|
|
446
|
+
cache.set(src, compiled);
|
|
447
|
+
return compiled;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Вычислить выражение: run(scope, fns) без явной компиляции. */
|
|
451
|
+
export function evaluateExpr(source, scope = {}, fns = {}) {
|
|
452
|
+
return compile(source).run(scope, fns);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/** Имена встроенных функций (для проверки правил и подсказок в редакторе). */
|
|
456
|
+
export const BUILTIN_FUNCTIONS = Object.freeze([...Object.keys(BUILTINS), 'if']);
|