@briza/illogical 2.0.2 → 2.2.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/lib/illogical.cjs +3324 -158
- package/lib/illogical.esm.js +3324 -158
- package/package.json +23 -4
- package/readme.md +5 -0
- package/types/bytecode/compiler.d.ts +29 -0
- package/types/bytecode/evaluable.d.ts +14 -0
- package/types/bytecode/get-bytecode.d.ts +1 -0
- package/types/bytecode/interpreter.d.ts +10 -0
- package/types/bytecode/opcodes.d.ts +51 -0
- package/types/bytecode/operateWithExpectedDecimals.d.ts +1 -0
- package/types/bytecode/refs.d.ts +73 -0
- package/types/bytecode/simplifier.d.ts +16 -0
- package/types/common/type-check.d.ts +7 -2
- package/types/common/util.d.ts +6 -0
- package/types/expression/arithmetic/dateArithmeticTypeCheck.d.ts +2 -0
- package/types/expression/arithmetic/index.d.ts +2 -0
- package/types/expression/arithmetic/mutateDateWithDuration.d.ts +2 -0
- package/types/index.d.ts +5 -1
- package/types/parser/options.d.ts +12 -2
package/lib/illogical.esm.js
CHANGED
|
@@ -21,6 +21,30 @@ function _toPropertyKey(t) {
|
|
|
21
21
|
return "symbol" == typeof i ? i : i + "";
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Valid types for context members
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Evaluation Context
|
|
30
|
+
* Holds references used during the evaluation process.
|
|
31
|
+
* Format: key: value.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Evaluation result
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
let EvaluableType = /*#__PURE__*/function (EvaluableType) {
|
|
39
|
+
EvaluableType["Operand"] = "Operand";
|
|
40
|
+
EvaluableType["Expression"] = "Expression";
|
|
41
|
+
return EvaluableType;
|
|
42
|
+
}({});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Evaluable
|
|
46
|
+
*/
|
|
47
|
+
|
|
24
48
|
/**
|
|
25
49
|
* Is number predicate.
|
|
26
50
|
* @param value Tested value.
|
|
@@ -93,30 +117,71 @@ function areAllResults(values) {
|
|
|
93
117
|
function areAllNumbers(results) {
|
|
94
118
|
return results.every(isNumber);
|
|
95
119
|
}
|
|
120
|
+
function isReference(operand) {
|
|
121
|
+
return operand.constructor.name === 'Reference';
|
|
122
|
+
}
|
|
123
|
+
function isValue(operand) {
|
|
124
|
+
return operand.constructor.name === 'Value';
|
|
125
|
+
}
|
|
96
126
|
|
|
97
127
|
/**
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Evaluation Context
|
|
103
|
-
* Holds references used during the evaluation process.
|
|
104
|
-
* Format: key: value.
|
|
128
|
+
* Convert a value to number if possible, otherwise return undefined
|
|
129
|
+
* @param value value to be converted to number
|
|
105
130
|
*/
|
|
131
|
+
const toNumber = value => {
|
|
132
|
+
const isValueNumber = isNumber(value);
|
|
133
|
+
if (isValueNumber) {
|
|
134
|
+
return value;
|
|
135
|
+
} else if (isString(value)) {
|
|
136
|
+
if (value.match(/^\d+\.\d+$/)) {
|
|
137
|
+
return parseFloat(value);
|
|
138
|
+
} else if (value.match(/^0$|^[1-9]\d*$/)) {
|
|
139
|
+
return parseInt(value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
};
|
|
106
144
|
|
|
107
145
|
/**
|
|
108
|
-
*
|
|
146
|
+
* Convert a value to string if possible, otherwise return undefined
|
|
147
|
+
* @param value value to be converted to string
|
|
109
148
|
*/
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
149
|
+
const toString = value => {
|
|
150
|
+
if (isNumber(value)) {
|
|
151
|
+
return `${value}`;
|
|
152
|
+
} else if (isString(value)) {
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
return undefined;
|
|
156
|
+
};
|
|
117
157
|
/**
|
|
118
|
-
*
|
|
158
|
+
* Convert a value to number if it's type is string, otherwise return NaN
|
|
159
|
+
* @param value value to be converted to number
|
|
119
160
|
*/
|
|
161
|
+
const toDateNumber = value => {
|
|
162
|
+
if (isString(value)) {
|
|
163
|
+
return Date.parse(value);
|
|
164
|
+
}
|
|
165
|
+
return NaN;
|
|
166
|
+
};
|
|
167
|
+
const formatDateNumber = dateNumber => new Date(dateNumber).toISOString().split('T')[0];
|
|
168
|
+
const toDateDuration = value => {
|
|
169
|
+
if (!isString(value)) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const isDateDuration = value.match(/^([1-9]\d*)([dmy])$/);
|
|
173
|
+
if (!isDateDuration) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const unit = isDateDuration[2];
|
|
177
|
+
if (unit !== 'd' && unit !== 'm' && unit !== 'y') {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
amount: parseInt(isDateDuration[1], 10),
|
|
182
|
+
unit
|
|
183
|
+
};
|
|
184
|
+
};
|
|
120
185
|
|
|
121
186
|
/**
|
|
122
187
|
* Abstract arithmetic expression
|
|
@@ -155,6 +220,21 @@ class Arithmetic {
|
|
|
155
220
|
}
|
|
156
221
|
return presentValues;
|
|
157
222
|
}
|
|
223
|
+
getDateCalculationResults(results) {
|
|
224
|
+
const [date, ...durations] = results;
|
|
225
|
+
const dateNumber = toDateNumber(date);
|
|
226
|
+
if (isNaN(dateNumber)) {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
const parsedDurations = durations.flatMap(d => {
|
|
230
|
+
const parsedDuration = toDateDuration(d);
|
|
231
|
+
return parsedDuration ? [parsedDuration] : [];
|
|
232
|
+
});
|
|
233
|
+
if (parsedDurations.length !== durations.length) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
return [dateNumber, ...parsedDurations];
|
|
237
|
+
}
|
|
158
238
|
|
|
159
239
|
/**
|
|
160
240
|
* Performs the arithmetic operation on the operands evaluated values.
|
|
@@ -233,20 +313,20 @@ class Divide extends Arithmetic {
|
|
|
233
313
|
}
|
|
234
314
|
}
|
|
235
315
|
|
|
236
|
-
const getNumDecimals = num => {
|
|
316
|
+
const getNumDecimals$1 = num => {
|
|
237
317
|
const numberSplit = num.toString().split('.');
|
|
238
318
|
return numberSplit.length == 2 ? numberSplit[1].length : 0;
|
|
239
319
|
};
|
|
240
|
-
const operateWithExpectedDecimals = operation => (first, second) => {
|
|
241
|
-
const numDecimals1 = getNumDecimals(first);
|
|
242
|
-
const numDecimals2 = getNumDecimals(second);
|
|
320
|
+
const operateWithExpectedDecimals$1 = operation => (first, second) => {
|
|
321
|
+
const numDecimals1 = getNumDecimals$1(first);
|
|
322
|
+
const numDecimals2 = getNumDecimals$1(second);
|
|
243
323
|
const maxDecimals = operation === 'multiply' ? numDecimals1 + numDecimals2 : numDecimals1 > numDecimals2 ? numDecimals1 : numDecimals2;
|
|
244
324
|
return operation === 'sum' ? Number((first + second).toFixed(maxDecimals)) : operation === 'subtract' ? Number((first - second).toFixed(maxDecimals)) : Number((first * second).toFixed(maxDecimals));
|
|
245
325
|
};
|
|
246
326
|
|
|
247
327
|
// Operator key
|
|
248
328
|
const OPERATOR$k = Symbol('MULTIPLY');
|
|
249
|
-
const multiplyWithExpectedDecimals = operateWithExpectedDecimals('multiply');
|
|
329
|
+
const multiplyWithExpectedDecimals = operateWithExpectedDecimals$1('multiply');
|
|
250
330
|
|
|
251
331
|
/**
|
|
252
332
|
* Multiply operation expression
|
|
@@ -276,9 +356,66 @@ class Multiply extends Arithmetic {
|
|
|
276
356
|
}
|
|
277
357
|
}
|
|
278
358
|
|
|
359
|
+
const dateArithmeticTypeCheck = (...operands) => {
|
|
360
|
+
const [first, ...rest] = operands;
|
|
361
|
+
const restAreAllReferences = rest.every(op => isReference(op));
|
|
362
|
+
const values = rest.filter(op => isValue(op));
|
|
363
|
+
const valuesAreAllDurations = values.every(op => !!toDateDuration(op.evaluate()));
|
|
364
|
+
const valuesAreAllNumbers = values.every(op => isNumber(op.evaluate()));
|
|
365
|
+
if (isReference(first)) {
|
|
366
|
+
if (!restAreAllReferences && !valuesAreAllDurations && !valuesAreAllNumbers) {
|
|
367
|
+
throw new Error('sum expression value literals should be all numbers or all date durations');
|
|
368
|
+
}
|
|
369
|
+
} else if (isValue(first)) {
|
|
370
|
+
if (isNumber(first.evaluate())) {
|
|
371
|
+
if (!restAreAllReferences && !valuesAreAllNumbers) {
|
|
372
|
+
throw new Error('sum expression value literals should be all numbers');
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
if (!isNaN(toDateNumber(first.evaluate()))) {
|
|
376
|
+
if (!restAreAllReferences && !valuesAreAllDurations) {
|
|
377
|
+
throw new Error('sum expression value literals should be all date durations');
|
|
378
|
+
}
|
|
379
|
+
} else {
|
|
380
|
+
throw new Error(
|
|
381
|
+
// eslint-disable-next-line max-len
|
|
382
|
+
'sum expression value literals should be all numbers or starting with an iso date string followed by date durations');
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
};
|
|
387
|
+
|
|
388
|
+
const mutateDateWithDuration = (dateNumber, duration, operator) => {
|
|
389
|
+
const {
|
|
390
|
+
amount,
|
|
391
|
+
unit
|
|
392
|
+
} = duration;
|
|
393
|
+
const date = new Date(dateNumber);
|
|
394
|
+
let targetYear = date.getUTCFullYear();
|
|
395
|
+
let targetMonthIndex = date.getUTCMonth();
|
|
396
|
+
const day = date.getUTCDate();
|
|
397
|
+
let targetDay = day;
|
|
398
|
+
if (unit === 'd') {
|
|
399
|
+
const targetDate = new Date(targetYear, targetMonthIndex, targetDay);
|
|
400
|
+
targetDate.setDate(operator === 'sum' ? targetDate.getDate() + amount : targetDate.getDate() - amount);
|
|
401
|
+
targetYear = targetDate.getFullYear();
|
|
402
|
+
targetMonthIndex = targetDate.getMonth();
|
|
403
|
+
targetDay = targetDate.getDate();
|
|
404
|
+
} else if (unit === 'm' || unit === 'y') {
|
|
405
|
+
if (unit === 'm') {
|
|
406
|
+
targetMonthIndex = operator === 'sum' ? targetMonthIndex + amount : targetMonthIndex - amount;
|
|
407
|
+
} else if (unit === 'y') {
|
|
408
|
+
targetYear = operator === 'sum' ? targetYear + amount : targetYear - amount;
|
|
409
|
+
}
|
|
410
|
+
const lastDayOfTargetMonth = new Date(targetYear, targetMonthIndex + 1, 0).getDate();
|
|
411
|
+
targetDay = Math.min(day, lastDayOfTargetMonth);
|
|
412
|
+
}
|
|
413
|
+
return new Date(Date.UTC(targetYear, targetMonthIndex, targetDay)).getTime();
|
|
414
|
+
};
|
|
415
|
+
|
|
279
416
|
// Operator key
|
|
280
417
|
const OPERATOR$j = Symbol('SUBTRACT');
|
|
281
|
-
const subtractWithExpectedDecimals = operateWithExpectedDecimals('subtract');
|
|
418
|
+
const subtractWithExpectedDecimals = operateWithExpectedDecimals$1('subtract');
|
|
282
419
|
|
|
283
420
|
/**
|
|
284
421
|
* Subtract operation expression
|
|
@@ -297,9 +434,15 @@ class Subtract extends Arithmetic {
|
|
|
297
434
|
if (operands.length < 2) {
|
|
298
435
|
throw new Error('subtract expression requires at least 2 operands');
|
|
299
436
|
}
|
|
437
|
+
dateArithmeticTypeCheck(...operands);
|
|
300
438
|
super('-', OPERATOR$j, operands);
|
|
301
439
|
}
|
|
302
440
|
operate(results) {
|
|
441
|
+
const dateCalculationResults = this.getDateCalculationResults(results);
|
|
442
|
+
if (dateCalculationResults) {
|
|
443
|
+
const [dateNumber, ...durations] = dateCalculationResults;
|
|
444
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, duration, 'subtract'), dateNumber));
|
|
445
|
+
}
|
|
303
446
|
const presentResults = this.getResultValues(results);
|
|
304
447
|
if (presentResults === false) {
|
|
305
448
|
return false;
|
|
@@ -310,7 +453,7 @@ class Subtract extends Arithmetic {
|
|
|
310
453
|
|
|
311
454
|
// Operator key
|
|
312
455
|
const OPERATOR$i = Symbol('SUM');
|
|
313
|
-
const addWithExpectedDecimals = operateWithExpectedDecimals('sum');
|
|
456
|
+
const addWithExpectedDecimals = operateWithExpectedDecimals$1('sum');
|
|
314
457
|
|
|
315
458
|
/**
|
|
316
459
|
* Sum operation expression
|
|
@@ -329,9 +472,15 @@ class Sum extends Arithmetic {
|
|
|
329
472
|
if (operands.length < 2) {
|
|
330
473
|
throw new Error('sum expression requires at least 2 operands');
|
|
331
474
|
}
|
|
475
|
+
dateArithmeticTypeCheck(...operands);
|
|
332
476
|
super('+', OPERATOR$i, operands);
|
|
333
477
|
}
|
|
334
478
|
operate(results) {
|
|
479
|
+
const dateCalculationResults = this.getDateCalculationResults(results);
|
|
480
|
+
if (dateCalculationResults) {
|
|
481
|
+
const [dateNumber, ...durations] = dateCalculationResults;
|
|
482
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, duration, 'sum'), dateNumber));
|
|
483
|
+
}
|
|
335
484
|
const presentResults = this.getResultValues(results);
|
|
336
485
|
if (presentResults === false) {
|
|
337
486
|
return false;
|
|
@@ -370,6 +519,17 @@ class Operand {
|
|
|
370
519
|
* @return {string}
|
|
371
520
|
*/
|
|
372
521
|
function printValue(value) {
|
|
522
|
+
const dateDuration = toDateDuration(value);
|
|
523
|
+
if (dateDuration) {
|
|
524
|
+
switch (dateDuration.unit) {
|
|
525
|
+
case 'd':
|
|
526
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'days' : 'day'}"`;
|
|
527
|
+
case 'm':
|
|
528
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'months' : 'month'}"`;
|
|
529
|
+
case 'y':
|
|
530
|
+
return `"${dateDuration.amount} ${dateDuration.amount > 1 ? 'years' : 'year'}"`;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
373
533
|
if (isString(value)) {
|
|
374
534
|
return `"${value}"`;
|
|
375
535
|
}
|
|
@@ -411,6 +571,7 @@ class Value extends Operand {
|
|
|
411
571
|
* {@link Evaluable.serialize}
|
|
412
572
|
*/
|
|
413
573
|
serialize() {
|
|
574
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
|
|
414
575
|
return this.value;
|
|
415
576
|
}
|
|
416
577
|
|
|
@@ -423,62 +584,21 @@ class Value extends Operand {
|
|
|
423
584
|
}
|
|
424
585
|
}
|
|
425
586
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
*/
|
|
430
|
-
const toNumber = value => {
|
|
431
|
-
const isValueNumber = isNumber(value);
|
|
432
|
-
if (isValueNumber) {
|
|
433
|
-
return value;
|
|
434
|
-
} else if (isString(value)) {
|
|
435
|
-
if (value.match(/^\d+\.\d+$/)) {
|
|
436
|
-
return parseFloat(value);
|
|
437
|
-
} else if (value.match(/^0$|^[1-9]\d*$/)) {
|
|
438
|
-
return parseInt(value);
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
return undefined;
|
|
442
|
-
};
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* Convert a value to string if possible, otherwise return undefined
|
|
446
|
-
* @param value value to be converted to string
|
|
447
|
-
*/
|
|
448
|
-
const toString = value => {
|
|
449
|
-
if (isNumber(value)) {
|
|
450
|
-
return `${value}`;
|
|
451
|
-
} else if (isString(value)) {
|
|
452
|
-
return value;
|
|
453
|
-
}
|
|
454
|
-
return undefined;
|
|
455
|
-
};
|
|
456
|
-
/**
|
|
457
|
-
* Convert a value to number if it's type is string, otherwise return NaN
|
|
458
|
-
* @param value value to be converted to number
|
|
459
|
-
*/
|
|
460
|
-
const toDateNumber = value => {
|
|
461
|
-
if (isString(value)) {
|
|
462
|
-
return Date.parse(value);
|
|
463
|
-
}
|
|
464
|
-
return NaN;
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
const keyWithArrayIndexRegex = /^(?<currentKey>[^[\]]+?)(?<indexes>(?:\[\d+])+)?$/;
|
|
468
|
-
const arrayIndexRegex = /\[(\d+)]/g;
|
|
469
|
-
function parseBacktickWrappedKey(key) {
|
|
587
|
+
const keyWithArrayIndexRegex$1 = /^(?<currentKey>[^[\]]+?)(?<indexes>(?:\[\d+])+)?$/;
|
|
588
|
+
const arrayIndexRegex$1 = /\[(\d+)]/g;
|
|
589
|
+
function parseBacktickWrappedKey$1(key) {
|
|
470
590
|
return key[0] === '`' && key[key.length - 1] === '`' ? key.slice(1, -1) : key;
|
|
471
591
|
}
|
|
472
|
-
function parseKeyComponents(key) {
|
|
473
|
-
const unwrappedKey = parseBacktickWrappedKey(key);
|
|
592
|
+
function parseKeyComponents$1(key) {
|
|
593
|
+
const unwrappedKey = parseBacktickWrappedKey$1(key);
|
|
474
594
|
const keys = [];
|
|
475
|
-
const parseResult = keyWithArrayIndexRegex.exec(unwrappedKey);
|
|
595
|
+
const parseResult = keyWithArrayIndexRegex$1.exec(unwrappedKey);
|
|
476
596
|
if (parseResult) {
|
|
477
|
-
const extractedKey = parseBacktickWrappedKey(parseResult?.groups?.currentKey ?? unwrappedKey);
|
|
597
|
+
const extractedKey = parseBacktickWrappedKey$1(parseResult?.groups?.currentKey ?? unwrappedKey);
|
|
478
598
|
keys.push(extractedKey);
|
|
479
599
|
const rawIndexes = parseResult?.groups?.indexes;
|
|
480
600
|
if (rawIndexes) {
|
|
481
|
-
for (const indexResult of rawIndexes.matchAll(arrayIndexRegex)) {
|
|
601
|
+
for (const indexResult of rawIndexes.matchAll(arrayIndexRegex$1)) {
|
|
482
602
|
keys.push(parseInt(indexResult[1]));
|
|
483
603
|
}
|
|
484
604
|
}
|
|
@@ -487,10 +607,10 @@ function parseKeyComponents(key) {
|
|
|
487
607
|
}
|
|
488
608
|
return keys;
|
|
489
609
|
}
|
|
490
|
-
const parseKeyRegex = /(`[^[\]]+`(\[\d+\])*|[^`.]+)/g;
|
|
610
|
+
const parseKeyRegex$1 = /(`[^[\]]+`(\[\d+\])*|[^`.]+)/g;
|
|
491
611
|
function parseKey(key) {
|
|
492
|
-
const keys = key.match(parseKeyRegex);
|
|
493
|
-
return !keys ? [] : keys.flatMap(parseKeyComponents);
|
|
612
|
+
const keys = key.match(parseKeyRegex$1);
|
|
613
|
+
return !keys ? [] : keys.flatMap(parseKeyComponents$1);
|
|
494
614
|
}
|
|
495
615
|
const complexKeyExpression = /{([^{}]+)}/;
|
|
496
616
|
function extractComplexKeys(ctx, key) {
|
|
@@ -546,9 +666,12 @@ let DataType = /*#__PURE__*/function (DataType) {
|
|
|
546
666
|
}({});
|
|
547
667
|
|
|
548
668
|
// Equivalent to /^.+\.\((Number|String)\)$/
|
|
549
|
-
const dataTypeRegex = new RegExp(`^.+\\.\\((${Object.keys(DataType).join('|')})\\)$`);
|
|
669
|
+
const dataTypeRegex$1 = new RegExp(`^.+\\.\\((${Object.keys(DataType).join('|')})\\)$`);
|
|
550
670
|
const isComplexKey = key => key.indexOf('{') > -1;
|
|
551
|
-
|
|
671
|
+
function isDataTypeKey$1(k) {
|
|
672
|
+
return k in DataType;
|
|
673
|
+
}
|
|
674
|
+
const castingRegex$1 = /\.\(.+\)$/;
|
|
552
675
|
|
|
553
676
|
/**
|
|
554
677
|
* Reference operand resolved within the context
|
|
@@ -568,10 +691,13 @@ class Reference extends Operand {
|
|
|
568
691
|
_defineProperty(this, "valueLookup", void 0);
|
|
569
692
|
_defineProperty(this, "getKeys", void 0);
|
|
570
693
|
this.key = key;
|
|
571
|
-
const dataTypeMatch = dataTypeRegex.exec(this.key);
|
|
694
|
+
const dataTypeMatch = dataTypeRegex$1.exec(this.key);
|
|
572
695
|
if (dataTypeMatch) {
|
|
573
|
-
|
|
574
|
-
|
|
696
|
+
const dtKey = dataTypeMatch[1];
|
|
697
|
+
if (isDataTypeKey$1(dtKey)) {
|
|
698
|
+
this.dataType = DataType[dtKey];
|
|
699
|
+
}
|
|
700
|
+
this.key = this.key.replace(castingRegex$1, '');
|
|
575
701
|
}
|
|
576
702
|
if (isComplexKey(this.key)) {
|
|
577
703
|
this.valueLookup = context => complexValueLookup(context, this.key);
|
|
@@ -859,10 +985,13 @@ class In extends Comparison {
|
|
|
859
985
|
if (!leftArray && !rightArray) {
|
|
860
986
|
throw new Error('invalid IN expression, non of the operands is array');
|
|
861
987
|
}
|
|
862
|
-
if (
|
|
988
|
+
if (Array.isArray(left)) {
|
|
863
989
|
return left.indexOf(right) > -1;
|
|
864
990
|
}
|
|
865
|
-
|
|
991
|
+
if (Array.isArray(right)) {
|
|
992
|
+
return right.indexOf(left) > -1;
|
|
993
|
+
}
|
|
994
|
+
return false;
|
|
866
995
|
}
|
|
867
996
|
|
|
868
997
|
/**
|
|
@@ -1014,10 +1143,13 @@ class NotIn extends Comparison {
|
|
|
1014
1143
|
if (!leftArray && !rightArray) {
|
|
1015
1144
|
throw new Error('invalid NOT IN expression, one operand must be array');
|
|
1016
1145
|
}
|
|
1017
|
-
if (
|
|
1146
|
+
if (Array.isArray(left)) {
|
|
1018
1147
|
return left.indexOf(right) === -1;
|
|
1019
1148
|
}
|
|
1020
|
-
|
|
1149
|
+
if (Array.isArray(right)) {
|
|
1150
|
+
return right.indexOf(left) === -1;
|
|
1151
|
+
}
|
|
1152
|
+
return true;
|
|
1021
1153
|
}
|
|
1022
1154
|
|
|
1023
1155
|
/**
|
|
@@ -1064,12 +1196,10 @@ class Overlap extends Comparison {
|
|
|
1064
1196
|
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
1065
1197
|
throw new Error('invalid OVERLAP expression, both operands must be array');
|
|
1066
1198
|
}
|
|
1067
|
-
|
|
1068
|
-
const rightArray = right;
|
|
1069
|
-
if (leftArray.length === 0 && rightArray.length === 0) {
|
|
1199
|
+
if (left.length === 0 && right.length === 0) {
|
|
1070
1200
|
return true;
|
|
1071
1201
|
}
|
|
1072
|
-
return
|
|
1202
|
+
return left.some(element => right.includes(element));
|
|
1073
1203
|
}
|
|
1074
1204
|
|
|
1075
1205
|
/**
|
|
@@ -1568,11 +1698,8 @@ class Xor extends Logical {
|
|
|
1568
1698
|
evaluate(ctx) {
|
|
1569
1699
|
let res = null;
|
|
1570
1700
|
for (const operand of this.operands) {
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
} else {
|
|
1574
|
-
res = xor(res, operand.evaluate(ctx));
|
|
1575
|
-
}
|
|
1701
|
+
const val = Boolean(operand.evaluate(ctx));
|
|
1702
|
+
res = res === null ? val : xor(res, val);
|
|
1576
1703
|
}
|
|
1577
1704
|
return res;
|
|
1578
1705
|
}
|
|
@@ -1614,81 +1741,3099 @@ class Xor extends Logical {
|
|
|
1614
1741
|
}
|
|
1615
1742
|
|
|
1616
1743
|
/**
|
|
1617
|
-
*
|
|
1744
|
+
* Bytecode opcodes for the illogical expression interpreter.
|
|
1745
|
+
*
|
|
1746
|
+
* Each opcode is a small integer. Some opcodes consume the next element(s)
|
|
1747
|
+
* in the bytecode array as operand data (e.g. OP_PUSH_VALUE reads the next
|
|
1748
|
+
* element as the literal value to push).
|
|
1618
1749
|
*/
|
|
1619
|
-
class Collection extends Operand {
|
|
1620
|
-
/**
|
|
1621
|
-
* Get the items in the collection.
|
|
1622
|
-
* @returns {Array<Value | Reference>}
|
|
1623
|
-
*/
|
|
1624
|
-
getItems() {
|
|
1625
|
-
return this.items;
|
|
1626
|
-
}
|
|
1627
1750
|
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1751
|
+
const OP_PUSH_VALUE = 1; // next: literal — push literal onto stack
|
|
1752
|
+
const OP_PUSH_REF_KEY = 6; // next: index into refs (CompactRef string) — ctx[key]
|
|
1753
|
+
const OP_PUSH_REF_KEYS = 7; // next: index into refs (CompactRef string[]) — inline multi-key walk
|
|
1754
|
+
const OP_PUSH_REF_TOKENS = 8; // next: index into refs (CompactRef token/dataType obj) — token walk
|
|
1755
|
+
const OP_PUSH_REF_DYNAMIC = 9; // next: index into refs (CompactRef dynamic obj) — runtime substitution
|
|
1756
|
+
const OP_MAKE_COLLECTION = 3; // next: N — pop N items, push as array
|
|
1757
|
+
const OP_PUSH_CONST = 4; // next: constIdx — push consts[constIdx] (static array) onto stack
|
|
1758
|
+
const OP_OVERLAP_CONST = 5; // next: constIdx — pop dynamic array, Set-intersect against consts[constIdx]
|
|
1759
|
+
|
|
1760
|
+
const OP_EQ = 10; // pop 2, push (left === right)
|
|
1761
|
+
const OP_NE = 11; // pop 2, push (left !== right)
|
|
1762
|
+
const OP_GT = 12; // pop 2, push (left > right), with date fallback
|
|
1763
|
+
const OP_GE = 13; // pop 2, push (left >= right), with date fallback
|
|
1764
|
+
const OP_LT = 14; // pop 2, push (left < right), with date fallback
|
|
1765
|
+
const OP_LE = 15; // pop 2, push (left <= right), with date fallback
|
|
1766
|
+
const OP_IN = 16; // pop 2, push membership check (one must be array) — dynamic fallback
|
|
1767
|
+
const OP_NOT_IN = 17; // pop 2, push non-membership check — dynamic fallback
|
|
1768
|
+
const OP_IN_COLLECTION = 45; // next: N — pop scalar, scan N stack items, push membership result
|
|
1769
|
+
const OP_NOT_IN_COLLECTION = 46; // next: N — pop scalar, scan N stack items, push non-membership result
|
|
1770
|
+
const OP_IN_CONST = 49; // next: constIdx — pop scalar, Set-lookup in consts[constIdx], push membership result
|
|
1771
|
+
const OP_NOT_IN_CONST = 50; // next: constIdx — pop scalar, Set-lookup in consts[constIdx], push !membership
|
|
1772
|
+
// next: N, ref0..refN-1, constIdx — resolve each ref inline against Set, no stack alloc
|
|
1773
|
+
const OP_OVERLAP_SCAN_REFS_CONST = 51;
|
|
1774
|
+
// next: ref1Idx, ref2Idx, M, v0, setBIdx0, v1, setBIdx1, ..., vM-1, setBIdxM-1
|
|
1775
|
+
// Inverted-index form: M entries of (literal setA value, constIdx of merged setB).
|
|
1776
|
+
// At runtime: resolve ref1, look up matching entries by value (O(1) via cached Map),
|
|
1777
|
+
// for each match check ref2 ∈ mergedSetB. Refs resolved once; setBs union-merged per setA value.
|
|
1778
|
+
const OP_OR_AND_IN_CONST_2 = 52;
|
|
1779
|
+
const OP_PREFIX = 18; // pop 2, push right.startsWith(left)
|
|
1780
|
+
const OP_SUFFIX = 19; // pop 2, push left.endsWith(right)
|
|
1781
|
+
const OP_OVERLAP = 20; // pop 2, push array intersection check
|
|
1782
|
+
const OP_PRESENT = 21; // pop 1, push (value !== undefined && value !== null)
|
|
1783
|
+
const OP_UNDEFINED = 22; // pop 1, push (value === undefined)
|
|
1784
|
+
|
|
1785
|
+
const OP_SUM = 30; // next: N — pop N numbers, push decimal-corrected sum
|
|
1786
|
+
const OP_SUBTRACT = 31; // next: N — pop N numbers, push decimal-corrected subtraction
|
|
1787
|
+
const OP_MULTIPLY = 32; // next: N — pop N numbers, push decimal-corrected multiplication
|
|
1788
|
+
const OP_DIVIDE = 33; // next: N — pop N numbers, push division result
|
|
1789
|
+
|
|
1790
|
+
const OP_STORE_LOCAL = 47; // next: slot — peek top of stack, store into locals[slot]
|
|
1791
|
+
const OP_LOAD_LOCAL = 48; // next: slot — push locals[slot] onto stack
|
|
1792
|
+
|
|
1793
|
+
const OP_NOT = 40; // pop 1 boolean, push negation
|
|
1794
|
+
const OP_JUMP_IF_FALSE = 41; // next: offset — peek stack; if false jump forward by offset
|
|
1795
|
+
const OP_JUMP_IF_TRUE = 42; // next: offset — peek stack; if true jump forward by offset
|
|
1796
|
+
const OP_POP = 43; // pop and discard top of stack
|
|
1797
|
+
const OP_XOR = 44; // pop 2 booleans, push (a || b) && !(a && b)
|
|
1798
|
+
|
|
1799
|
+
// Logical marker opcodes — emitted after the short-circuit sequence.
|
|
1800
|
+
// The evaluate interpreter skips them (consumes the operand count byte).
|
|
1801
|
+
// The simplify interpreter uses them to identify the operator and arity for residual reconstruction.
|
|
1802
|
+
const OP_AND = 53; // next: N — marker for AND with N operands
|
|
1803
|
+
const OP_OR = 54; // next: N — marker for OR with N operands
|
|
1804
|
+
const OP_NOR = 55; // next: N — marker for NOR with N operands
|
|
1805
|
+
// next: N, ref0..refN-1, constIdx — resolve each ref inline, check ∈ consts[constIdx], no stack alloc
|
|
1806
|
+
const OP_IN_SCAN_REFS_CONST = 56;
|
|
1807
|
+
// next: N, ref0..refN-1, constIdx — resolve each ref inline, check ∉ consts[constIdx], no stack alloc
|
|
1808
|
+
const OP_NOT_IN_SCAN_REFS_CONST = 57;
|
|
1637
1809
|
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1810
|
+
/**
|
|
1811
|
+
* Reference path descriptors for the bytecode compiler and interpreter.
|
|
1812
|
+
*
|
|
1813
|
+
* CompactRef is the single runtime type stored in CompiledExpression.refs.
|
|
1814
|
+
* The compiler picks the right opcode per ref kind so the interpreter
|
|
1815
|
+
* dispatches to a dedicated handler with no secondary branching.
|
|
1816
|
+
*
|
|
1817
|
+
* string — OP_PUSH_REF_KEY — ctx[key]
|
|
1818
|
+
* string[] — OP_PUSH_REF_KEYS — inline multi-key walk
|
|
1819
|
+
* CompactRefFull (tokens) — OP_PUSH_REF_TOKENS — token walk + optional cast
|
|
1820
|
+
* CompactRefFull (dynamic) — OP_PUSH_REF_DYNAMIC — runtime {placeholder} substitution
|
|
1821
|
+
*/
|
|
1646
1822
|
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1823
|
+
|
|
1824
|
+
// array index: [0]
|
|
1825
|
+
|
|
1826
|
+
/** Full compact form for refs that don't fit the short string/string[] forms. */
|
|
1827
|
+
|
|
1828
|
+
/**
|
|
1829
|
+
* Runtime ref representation stored in CompiledExpression.refs:
|
|
1830
|
+
* - `string` → single plain-key (most common)
|
|
1831
|
+
* - `string[]` → multi-key inline path
|
|
1832
|
+
* - `CompactRefFull` → token-based or dynamic
|
|
1833
|
+
*/
|
|
1834
|
+
|
|
1835
|
+
// ---------------------------------------------------------------------------
|
|
1836
|
+
// Regex — parsed once at compile time
|
|
1837
|
+
// ---------------------------------------------------------------------------
|
|
1838
|
+
const keyWithArrayIndexRegex = /^(?<currentKey>[^[\]]+?)(?<indexes>(?:\[\d+])+)?$/;
|
|
1839
|
+
const arrayIndexRegex = /\[(\d+)]/g;
|
|
1840
|
+
const parseKeyRegex = /(`[^[\]]+`(\[\d+\])*|[^`.]+)/g;
|
|
1841
|
+
const dataTypeRegex = new RegExp(`^.+\\.\\((${Object.keys(DataType).join('|')})\\)$`);
|
|
1842
|
+
const castingRegex = /\.\(.+\)$/;
|
|
1843
|
+
function parseBacktickWrappedKey(key) {
|
|
1844
|
+
return key[0] === '`' && key[key.length - 1] === '`' ? key.slice(1, -1) : key;
|
|
1845
|
+
}
|
|
1846
|
+
function parseKeyComponents(key) {
|
|
1847
|
+
const unwrapped = parseBacktickWrappedKey(key);
|
|
1848
|
+
const tokens = [];
|
|
1849
|
+
const match = keyWithArrayIndexRegex.exec(unwrapped);
|
|
1850
|
+
if (match) {
|
|
1851
|
+
tokens.push({
|
|
1852
|
+
kind: 'key',
|
|
1853
|
+
value: parseBacktickWrappedKey(match.groups?.currentKey ?? unwrapped)
|
|
1854
|
+
});
|
|
1855
|
+
const rawIndexes = match.groups?.indexes;
|
|
1856
|
+
if (rawIndexes) {
|
|
1857
|
+
for (const idxMatch of rawIndexes.matchAll(arrayIndexRegex)) {
|
|
1858
|
+
tokens.push({
|
|
1859
|
+
kind: 'index',
|
|
1860
|
+
value: parseInt(idxMatch[1])
|
|
1861
|
+
});
|
|
1656
1862
|
}
|
|
1657
|
-
values.push(simplifiedItem);
|
|
1658
1863
|
}
|
|
1659
|
-
|
|
1864
|
+
} else {
|
|
1865
|
+
tokens.push({
|
|
1866
|
+
kind: 'key',
|
|
1867
|
+
value: unwrapped
|
|
1868
|
+
});
|
|
1660
1869
|
}
|
|
1870
|
+
return tokens;
|
|
1871
|
+
}
|
|
1872
|
+
function parseStaticKey(key) {
|
|
1873
|
+
const parts = key.match(parseKeyRegex);
|
|
1874
|
+
return !parts ? [] : parts.flatMap(parseKeyComponents);
|
|
1875
|
+
}
|
|
1876
|
+
function isDataTypeKey(k) {
|
|
1877
|
+
return k in DataType;
|
|
1878
|
+
}
|
|
1661
1879
|
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1880
|
+
/**
|
|
1881
|
+
* Build a CompactRef from a raw reference key string (without the $ prefix).
|
|
1882
|
+
* Called once at compile time; the result is stored in CompiledExpression.refs.
|
|
1883
|
+
*/
|
|
1884
|
+
function buildCompactRef(rawKey) {
|
|
1885
|
+
let key = rawKey;
|
|
1886
|
+
let dataType;
|
|
1887
|
+
const dataTypeMatch = dataTypeRegex.exec(key);
|
|
1888
|
+
if (dataTypeMatch) {
|
|
1889
|
+
const dtKey = dataTypeMatch[1];
|
|
1890
|
+
if (!isDataTypeKey(dtKey)) {
|
|
1891
|
+
throw new Error(`unknown DataType: ${dtKey}`);
|
|
1892
|
+
}
|
|
1893
|
+
dataType = DataType[dtKey];
|
|
1894
|
+
key = key.replace(castingRegex, '');
|
|
1895
|
+
}
|
|
1896
|
+
const hasDynamic = key.indexOf('{') > -1;
|
|
1897
|
+
if (hasDynamic) {
|
|
1898
|
+
const full = {
|
|
1899
|
+
k: key,
|
|
1900
|
+
d: true
|
|
1901
|
+
};
|
|
1902
|
+
if (dataType !== undefined) {
|
|
1903
|
+
full.t = dataType;
|
|
1904
|
+
}
|
|
1905
|
+
return full;
|
|
1667
1906
|
}
|
|
1907
|
+
const tokens = parseStaticKey(key);
|
|
1668
1908
|
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
return
|
|
1909
|
+
// Pure key-only path with no dataType → string or string[]
|
|
1910
|
+
if (!dataType && tokens.every(t => t.kind === 'key')) {
|
|
1911
|
+
if (tokens.length === 1) {
|
|
1912
|
+
return tokens[0].value;
|
|
1913
|
+
}
|
|
1914
|
+
return tokens.map(t => t.value);
|
|
1675
1915
|
}
|
|
1676
|
-
}
|
|
1677
1916
|
|
|
1678
|
-
//
|
|
1917
|
+
// Token-based path (array indexes) or dataType cast
|
|
1918
|
+
const full = {
|
|
1919
|
+
tokens
|
|
1920
|
+
};
|
|
1921
|
+
if (dataType !== undefined) {
|
|
1922
|
+
full.t = dataType;
|
|
1923
|
+
}
|
|
1924
|
+
return full;
|
|
1925
|
+
}
|
|
1679
1926
|
|
|
1680
|
-
//
|
|
1927
|
+
// ---------------------------------------------------------------------------
|
|
1928
|
+
// Runtime resolution — called by the interpreter on every evaluate()
|
|
1929
|
+
// ---------------------------------------------------------------------------
|
|
1681
1930
|
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1931
|
+
function castValue(value, dataType) {
|
|
1932
|
+
let result;
|
|
1933
|
+
if (dataType === DataType.Number) {
|
|
1934
|
+
result = toNumber(value);
|
|
1935
|
+
} else {
|
|
1936
|
+
result = toString(value);
|
|
1937
|
+
}
|
|
1938
|
+
if (value && result === undefined) {
|
|
1939
|
+
console.warn(`Casting ${value} to ${dataType} resulted in ${result}`);
|
|
1940
|
+
}
|
|
1941
|
+
return result;
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
// Matches the innermost {ref} — non-nested braces only
|
|
1945
|
+
const dynamicKeyRegex = /{([^{}]+)}/;
|
|
1946
|
+
|
|
1947
|
+
/** Type-safe property access on an object narrowed from `unknown`. */
|
|
1948
|
+
function propAt(obj, key) {
|
|
1949
|
+
return Reflect.get(obj, key);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
/** Narrow a CompactRef to its string form (OP_PUSH_REF_KEY). Throws on mismatch. */
|
|
1953
|
+
function asKeyRef(ref) {
|
|
1954
|
+
if (typeof ref !== 'string') {
|
|
1955
|
+
throw new Error('bytecode integrity: expected string ref');
|
|
1956
|
+
}
|
|
1957
|
+
return ref;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
/** Narrow a CompactRef to its string[] form (OP_PUSH_REF_KEYS). Throws on mismatch. */
|
|
1961
|
+
function asKeysRef(ref) {
|
|
1962
|
+
if (!Array.isArray(ref)) {
|
|
1963
|
+
throw new Error('bytecode integrity: expected string[] ref');
|
|
1964
|
+
}
|
|
1965
|
+
return ref;
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
/** Narrow a CompactRef to CompactRefFull (OP_PUSH_REF_TOKENS / OP_PUSH_REF_DYNAMIC). Throws on mismatch. */
|
|
1969
|
+
function asFullRef(ref) {
|
|
1970
|
+
if (typeof ref === 'string' || Array.isArray(ref)) {
|
|
1971
|
+
throw new Error('bytecode integrity: expected CompactRefFull ref');
|
|
1972
|
+
}
|
|
1973
|
+
return ref;
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
/**
|
|
1977
|
+
* Resolve a multi-key inline path (string[]) against a context.
|
|
1978
|
+
* Used by OP_PUSH_REF_KEYS and OP_PUSH_REF_DYNAMIC (after substitution).
|
|
1979
|
+
*/
|
|
1980
|
+
function resolveKeys(ks, ctx) {
|
|
1981
|
+
const p0 = ctx[ks[0]];
|
|
1982
|
+
if (ks.length === 1) {
|
|
1983
|
+
return p0;
|
|
1984
|
+
}
|
|
1985
|
+
if (p0 === undefined || p0 === null || typeof p0 !== 'object' || Array.isArray(p0)) {
|
|
1986
|
+
return undefined;
|
|
1987
|
+
}
|
|
1988
|
+
const p1 = propAt(p0, ks[1]);
|
|
1989
|
+
if (ks.length === 2) {
|
|
1990
|
+
return p1;
|
|
1991
|
+
}
|
|
1992
|
+
if (p1 === undefined || p1 === null || typeof p1 !== 'object' || Array.isArray(p1)) {
|
|
1993
|
+
return undefined;
|
|
1994
|
+
}
|
|
1995
|
+
const p2 = propAt(p1, ks[2]);
|
|
1996
|
+
if (ks.length === 3) {
|
|
1997
|
+
return p2;
|
|
1998
|
+
}
|
|
1999
|
+
let p = p2;
|
|
2000
|
+
for (let k = 3; k < ks.length; k++) {
|
|
2001
|
+
if (p === undefined || p === null || typeof p !== 'object' || Array.isArray(p)) {
|
|
2002
|
+
return undefined;
|
|
2003
|
+
}
|
|
2004
|
+
p = propAt(p, ks[k]);
|
|
2005
|
+
}
|
|
2006
|
+
return p;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
/**
|
|
2010
|
+
* Resolve a token-based path against a context.
|
|
2011
|
+
* Used by OP_PUSH_REF_TOKENS.
|
|
2012
|
+
*/
|
|
2013
|
+
function resolveTokens(tokens, dataType, ctx) {
|
|
2014
|
+
const len = tokens.length;
|
|
2015
|
+
let pointer = ctx;
|
|
2016
|
+
for (let i = 0; i < len; i++) {
|
|
2017
|
+
const token = tokens[i];
|
|
2018
|
+
if (pointer === undefined || pointer === null) {
|
|
2019
|
+
return dataType !== undefined ? castValue(undefined, dataType) : undefined;
|
|
2020
|
+
}
|
|
2021
|
+
if (token.kind === 'key') {
|
|
2022
|
+
if (typeof pointer !== 'object' || Array.isArray(pointer)) {
|
|
2023
|
+
return dataType !== undefined ? castValue(undefined, dataType) : undefined;
|
|
2024
|
+
}
|
|
2025
|
+
pointer = propAt(pointer, token.value);
|
|
2026
|
+
} else {
|
|
2027
|
+
if (!Array.isArray(pointer)) {
|
|
2028
|
+
return dataType !== undefined ? castValue(undefined, dataType) : undefined;
|
|
2029
|
+
}
|
|
2030
|
+
pointer = pointer[token.value];
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
return dataType !== undefined ? castValue(pointer, dataType) : pointer;
|
|
2034
|
+
}
|
|
2035
|
+
|
|
2036
|
+
/**
|
|
2037
|
+
* Resolve a dynamic ref (with {placeholder} substitution) against a context.
|
|
2038
|
+
* Used by OP_PUSH_REF_DYNAMIC.
|
|
2039
|
+
*/
|
|
2040
|
+
function resolveDynamic(key, dataType, ctx) {
|
|
2041
|
+
let current = key;
|
|
2042
|
+
let match = dynamicKeyRegex.exec(current);
|
|
2043
|
+
while (match) {
|
|
2044
|
+
const inner = resolveTokens(parseStaticKey(match[1]), undefined, ctx);
|
|
2045
|
+
if (inner === undefined) {
|
|
2046
|
+
return undefined;
|
|
2047
|
+
}
|
|
2048
|
+
current = current.replace(dynamicKeyRegex, `${inner}`);
|
|
2049
|
+
match = dynamicKeyRegex.exec(current);
|
|
2050
|
+
}
|
|
2051
|
+
const tokens = parseStaticKey(current);
|
|
2052
|
+
const raw = resolveTokens(tokens, undefined, ctx);
|
|
2053
|
+
return dataType !== undefined ? castValue(raw, dataType) : raw;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
/**
|
|
2057
|
+
* Resolve any CompactRef against a context.
|
|
2058
|
+
* Used in ops that embed ref indices but weren't split by kind
|
|
2059
|
+
* (OP_OVERLAP_SCAN_REFS_CONST, OP_OR_AND_IN_CONST_2).
|
|
2060
|
+
*/
|
|
2061
|
+
function resolveCompactRef(ref, ctx) {
|
|
2062
|
+
if (typeof ref === 'string') {
|
|
2063
|
+
return ctx[ref];
|
|
2064
|
+
}
|
|
2065
|
+
if (Array.isArray(ref)) {
|
|
2066
|
+
return resolveKeys(ref, ctx);
|
|
2067
|
+
}
|
|
2068
|
+
if (ref.d) {
|
|
2069
|
+
return resolveDynamic(ref.k, ref.t, ctx);
|
|
2070
|
+
}
|
|
2071
|
+
return resolveTokens(ref.tokens ?? [], ref.t, ctx);
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
/**
|
|
2075
|
+
* Bytecode compiler.
|
|
2076
|
+
*
|
|
2077
|
+
* Transforms a raw ExpressionInput (plain array) into a flat Bytecode array
|
|
2078
|
+
* that the interpreter can execute. This runs once per unique expression;
|
|
2079
|
+
* the result should be cached and reused across evaluate() calls.
|
|
2080
|
+
*/
|
|
2081
|
+
|
|
2082
|
+
|
|
2083
|
+
// Bytecode is a flat array of numbers (opcodes and index operands) interspersed
|
|
2084
|
+
// with literal Result values (for OP_PUSH_VALUE).
|
|
2085
|
+
|
|
2086
|
+
// Lookup tables built once per Options instance — operator string → opcode.
|
|
2087
|
+
// Stored outside emitExpression so they are not recreated on every compile call.
|
|
2088
|
+
|
|
2089
|
+
function getOperator(m, op) {
|
|
2090
|
+
const v = m.get(op);
|
|
2091
|
+
if (v === undefined) {
|
|
2092
|
+
throw new Error(`operator mapping missing for symbol ${op.toString()}`);
|
|
2093
|
+
}
|
|
2094
|
+
return v;
|
|
2095
|
+
}
|
|
2096
|
+
function buildOperatorMaps(opts) {
|
|
2097
|
+
const m = opts.operatorMapping;
|
|
2098
|
+
const get = op => getOperator(m, op);
|
|
2099
|
+
return {
|
|
2100
|
+
binary: {
|
|
2101
|
+
[get(OPERATOR$h)]: OP_EQ,
|
|
2102
|
+
[get(OPERATOR$b)]: OP_NE,
|
|
2103
|
+
[get(OPERATOR$f)]: OP_GT,
|
|
2104
|
+
[get(OPERATOR$g)]: OP_GE,
|
|
2105
|
+
[get(OPERATOR$c)]: OP_LT,
|
|
2106
|
+
[get(OPERATOR$d)]: OP_LE,
|
|
2107
|
+
[get(OPERATOR$e)]: OP_IN,
|
|
2108
|
+
[get(OPERATOR$a)]: OP_NOT_IN,
|
|
2109
|
+
[get(OPERATOR$8)]: OP_PREFIX,
|
|
2110
|
+
[get(OPERATOR$6)]: OP_SUFFIX,
|
|
2111
|
+
[get(OPERATOR$9)]: OP_OVERLAP
|
|
2112
|
+
},
|
|
2113
|
+
arithmetic: {
|
|
2114
|
+
[get(OPERATOR$i)]: OP_SUM,
|
|
2115
|
+
[get(OPERATOR$j)]: OP_SUBTRACT,
|
|
2116
|
+
[get(OPERATOR$k)]: OP_MULTIPLY,
|
|
2117
|
+
[get(OPERATOR$l)]: OP_DIVIDE
|
|
2118
|
+
},
|
|
2119
|
+
presentOp: get(OPERATOR$7),
|
|
2120
|
+
undefinedOp: get(OPERATOR$5),
|
|
2121
|
+
andOp: get(OPERATOR$4),
|
|
2122
|
+
orOp: get(OPERATOR$1),
|
|
2123
|
+
norOp: get(OPERATOR$2),
|
|
2124
|
+
notOp: get(OPERATOR$3),
|
|
2125
|
+
xorOp: get(OPERATOR),
|
|
2126
|
+
inOp: get(OPERATOR$e),
|
|
2127
|
+
notInOp: get(OPERATOR$a),
|
|
2128
|
+
overlapOp: get(OPERATOR$9),
|
|
2129
|
+
eqOp: get(OPERATOR$h)
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
function isStaticCollection(raw, opts) {
|
|
2133
|
+
return Array.isArray(raw) && !raw.some(v => typeof v === 'string' && opts.referencePredicate(v));
|
|
2134
|
+
}
|
|
2135
|
+
function isPureRefCollection(raw, opts) {
|
|
2136
|
+
return Array.isArray(raw) && raw.length > 0 && raw.every(v => typeof v === 'string' && opts.referencePredicate(v));
|
|
2137
|
+
}
|
|
2138
|
+
function internConst(items, state) {
|
|
2139
|
+
const key = JSON.stringify(items);
|
|
2140
|
+
let idx = state.constIndex.get(key);
|
|
2141
|
+
if (idx === undefined) {
|
|
2142
|
+
idx = state.consts.length;
|
|
2143
|
+
state.consts.push(items);
|
|
2144
|
+
state.constIndex.set(key, idx);
|
|
2145
|
+
}
|
|
2146
|
+
return idx;
|
|
2147
|
+
}
|
|
2148
|
+
|
|
2149
|
+
/**
|
|
2150
|
+
* Returns the first context key for a multi-key ref (e.g. 'account' for $account.region),
|
|
2151
|
+
* or undefined for single-key refs (no heuristic needed) and dynamic refs (unknowable statically).
|
|
2152
|
+
* Used by the simplify interpreter to replicate OOP Reference.simplify()'s first-key check.
|
|
2153
|
+
*/
|
|
2154
|
+
function getFirstCtxKey(ref) {
|
|
2155
|
+
if (typeof ref === 'string') {
|
|
2156
|
+
return undefined; // single-key ref — OOP checks ctx[key] directly, no first-key shortcut
|
|
2157
|
+
}
|
|
2158
|
+
if (Array.isArray(ref)) {
|
|
2159
|
+
return ref[0]; // multi-key path: first element is the top-level context key
|
|
2160
|
+
}
|
|
2161
|
+
if (ref.d) {
|
|
2162
|
+
return undefined; // dynamic ref with {placeholder}: can't determine first key statically
|
|
2163
|
+
}
|
|
2164
|
+
const tokens = ref.tokens;
|
|
2165
|
+
if (tokens && tokens.length > 0 && tokens[0].kind === 'key') {
|
|
2166
|
+
return tokens[0].value;
|
|
2167
|
+
}
|
|
2168
|
+
return undefined;
|
|
2169
|
+
}
|
|
2170
|
+
function refOpcode(ref) {
|
|
2171
|
+
if (typeof ref === 'string') {
|
|
2172
|
+
return OP_PUSH_REF_KEY;
|
|
2173
|
+
}
|
|
2174
|
+
if (Array.isArray(ref)) {
|
|
2175
|
+
return OP_PUSH_REF_KEYS;
|
|
2176
|
+
}
|
|
2177
|
+
if (ref.d) {
|
|
2178
|
+
return OP_PUSH_REF_DYNAMIC;
|
|
2179
|
+
}
|
|
2180
|
+
return OP_PUSH_REF_TOKENS;
|
|
2181
|
+
}
|
|
2182
|
+
function internRef(raw, state) {
|
|
2183
|
+
const key = state.opts.referenceTransform(raw);
|
|
2184
|
+
let refIndex = state.refIndex.get(key);
|
|
2185
|
+
if (refIndex === undefined) {
|
|
2186
|
+
refIndex = state.refs.length;
|
|
2187
|
+
state.refs.push(buildCompactRef(key));
|
|
2188
|
+
state.refIndex.set(key, refIndex);
|
|
2189
|
+
state.refRawKeys.push(key);
|
|
2190
|
+
state.refKeys.push(raw);
|
|
2191
|
+
}
|
|
2192
|
+
return refIndex;
|
|
2193
|
+
}
|
|
2194
|
+
function emitOperand(raw, state) {
|
|
2195
|
+
const {
|
|
2196
|
+
bytecode,
|
|
2197
|
+
refs,
|
|
2198
|
+
opts
|
|
2199
|
+
} = state;
|
|
2200
|
+
if (Array.isArray(raw)) {
|
|
2201
|
+
const hasDynamic = raw.some(v => typeof v === 'string' && opts.referencePredicate(v));
|
|
2202
|
+
if (hasDynamic) {
|
|
2203
|
+
// CSE: if this dynamic collection has been seen before, reuse the cached local slot
|
|
2204
|
+
const cseKey = JSON.stringify(raw);
|
|
2205
|
+
const existingSlot = state.collectionCse.get(cseKey);
|
|
2206
|
+
if (existingSlot !== undefined) {
|
|
2207
|
+
bytecode.push(OP_LOAD_LOCAL, existingSlot);
|
|
2208
|
+
return;
|
|
2209
|
+
}
|
|
2210
|
+
// First occurrence: emit normally, then store result in a new local slot
|
|
2211
|
+
for (const item of raw) {
|
|
2212
|
+
emitOperand(item, state);
|
|
2213
|
+
}
|
|
2214
|
+
bytecode.push(OP_MAKE_COLLECTION, raw.length);
|
|
2215
|
+
const slot = state.numLocals++;
|
|
2216
|
+
state.collectionCse.set(cseKey, slot);
|
|
2217
|
+
bytecode.push(OP_STORE_LOCAL, slot);
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
// Static collection: intern into consts table, emit OP_PUSH_CONST
|
|
2222
|
+
bytecode.push(OP_PUSH_CONST, internConst(raw, state));
|
|
2223
|
+
return;
|
|
2224
|
+
}
|
|
2225
|
+
if (typeof raw === 'string' && opts.referencePredicate(raw)) {
|
|
2226
|
+
const refIndex = internRef(raw, state);
|
|
2227
|
+
bytecode.push(refOpcode(refs[refIndex]), refIndex);
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
bytecode.push(OP_PUSH_VALUE, raw);
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
/**
|
|
2234
|
+
* Return the ref key and static value array for a branch child that is either:
|
|
2235
|
+
* IN(ref, [v1, v2, ...]) — ref ∈ static collection
|
|
2236
|
+
* ==(ref, scalar) — ref === scalar, treated as IN with [scalar]
|
|
2237
|
+
* Returns null if the child does not match either form.
|
|
2238
|
+
*/
|
|
2239
|
+
function extractInLikeChild(ca, state) {
|
|
2240
|
+
const op = ca[0];
|
|
2241
|
+
const left = ca[1];
|
|
2242
|
+
if (typeof left !== 'string' || !state.opts.referencePredicate(left)) {
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
// left is narrowed to string by the guard above
|
|
2246
|
+
const rawRef = left;
|
|
2247
|
+
const refKey = state.opts.referenceTransform(rawRef);
|
|
2248
|
+
if (op === state.maps.inOp) {
|
|
2249
|
+
const right = ca[2];
|
|
2250
|
+
if (!isStaticCollection(right, state.opts)) {
|
|
2251
|
+
return null;
|
|
2252
|
+
}
|
|
2253
|
+
return {
|
|
2254
|
+
rawRef,
|
|
2255
|
+
refKey,
|
|
2256
|
+
vals: right
|
|
2257
|
+
};
|
|
2258
|
+
}
|
|
2259
|
+
|
|
2260
|
+
// == (EQ): scalar equality treated as single-element IN
|
|
2261
|
+
if (op === state.maps.eqOp) {
|
|
2262
|
+
const right = ca[2];
|
|
2263
|
+
if (typeof right === 'string' && state.opts.referencePredicate(right)) {
|
|
2264
|
+
return null;
|
|
2265
|
+
}
|
|
2266
|
+
if (Array.isArray(right)) {
|
|
2267
|
+
return null;
|
|
2268
|
+
}
|
|
2269
|
+
return {
|
|
2270
|
+
rawRef,
|
|
2271
|
+
refKey,
|
|
2272
|
+
vals: [right]
|
|
2273
|
+
};
|
|
2274
|
+
}
|
|
2275
|
+
return null;
|
|
2276
|
+
}
|
|
2277
|
+
|
|
2278
|
+
/**
|
|
2279
|
+
* Check whether an OR expression matches the pattern:
|
|
2280
|
+
* OR( AND(IN-like(ref1, set1), IN-like(ref2, set2)), ... )
|
|
2281
|
+
* where IN-like is either IN(ref, staticSet) or ==(ref, scalar),
|
|
2282
|
+
* and every branch uses the exact same two refs in the same order.
|
|
2283
|
+
*
|
|
2284
|
+
* Builds an inverted index: for each unique value in any setA, union-merges all
|
|
2285
|
+
* setB values across branches where that setA value appears, and emits one
|
|
2286
|
+
* (literal value, mergedSetBIdx) entry per distinct setA value.
|
|
2287
|
+
*
|
|
2288
|
+
* This lets the interpreter do a single O(1) Map lookup on ref1 to find all
|
|
2289
|
+
* relevant setB indices, instead of a linear scan through N setA Sets.
|
|
2290
|
+
* Returns null if the pattern does not match.
|
|
2291
|
+
*/
|
|
2292
|
+
function detectOrAndIn2Pattern(arr, state) {
|
|
2293
|
+
const nBranches = arr.length - 1;
|
|
2294
|
+
if (nBranches < 2) {
|
|
2295
|
+
return null;
|
|
2296
|
+
}
|
|
2297
|
+
let ref1Raw = null;
|
|
2298
|
+
let ref2Raw = null;
|
|
2299
|
+
let refKey1 = null;
|
|
2300
|
+
let refKey2 = null;
|
|
2301
|
+
|
|
2302
|
+
// Inverted index: each distinct setA value → merged setB values across all branches containing it
|
|
2303
|
+
const setBValsByAValue = new Map();
|
|
2304
|
+
for (let b = 1; b <= nBranches; b++) {
|
|
2305
|
+
const branch = arr[b];
|
|
2306
|
+
if (!Array.isArray(branch)) {
|
|
2307
|
+
return null;
|
|
2308
|
+
}
|
|
2309
|
+
if (branch[0] !== state.maps.andOp) {
|
|
2310
|
+
return null;
|
|
2311
|
+
}
|
|
2312
|
+
if (branch.length !== 3) {
|
|
2313
|
+
return null;
|
|
2314
|
+
}
|
|
2315
|
+
let extA = null;
|
|
2316
|
+
let extB = null;
|
|
2317
|
+
for (let c = 1; c <= 2; c++) {
|
|
2318
|
+
const child = branch[c];
|
|
2319
|
+
if (!Array.isArray(child)) {
|
|
2320
|
+
return null;
|
|
2321
|
+
}
|
|
2322
|
+
const extracted = extractInLikeChild(child, state);
|
|
2323
|
+
if (extracted === null) {
|
|
2324
|
+
return null;
|
|
2325
|
+
}
|
|
2326
|
+
const {
|
|
2327
|
+
rawRef,
|
|
2328
|
+
refKey
|
|
2329
|
+
} = extracted;
|
|
2330
|
+
if (c === 1) {
|
|
2331
|
+
extA = extracted;
|
|
2332
|
+
if (b === 1) {
|
|
2333
|
+
ref1Raw = rawRef;
|
|
2334
|
+
refKey1 = refKey;
|
|
2335
|
+
} else if (refKey !== refKey1) {
|
|
2336
|
+
return null;
|
|
2337
|
+
}
|
|
2338
|
+
} else {
|
|
2339
|
+
extB = extracted;
|
|
2340
|
+
if (b === 1) {
|
|
2341
|
+
ref2Raw = rawRef;
|
|
2342
|
+
refKey2 = refKey;
|
|
2343
|
+
} else if (refKey !== refKey2) {
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
}
|
|
2348
|
+
if (extA === null || extB === null) {
|
|
2349
|
+
return null;
|
|
2350
|
+
}
|
|
2351
|
+
|
|
2352
|
+
// For each value in setA, union-merge all setB values
|
|
2353
|
+
for (const aVal of extA.vals) {
|
|
2354
|
+
let setBVals = setBValsByAValue.get(aVal);
|
|
2355
|
+
if (setBVals === undefined) {
|
|
2356
|
+
setBVals = new Set();
|
|
2357
|
+
setBValsByAValue.set(aVal, setBVals);
|
|
2358
|
+
}
|
|
2359
|
+
for (const bVal of extB.vals) {
|
|
2360
|
+
setBVals.add(bVal);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
if (ref1Raw === null || ref2Raw === null) {
|
|
2365
|
+
return null;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
// Build entries: one (literal aVal, mergedSetBIdx) per distinct setA value
|
|
2369
|
+
const entries = [];
|
|
2370
|
+
for (const [aVal, setBVals] of setBValsByAValue) {
|
|
2371
|
+
const mergedSetB = [...setBVals].filter(v => v !== undefined);
|
|
2372
|
+
entries.push([aVal, internConst(mergedSetB, state)]);
|
|
2373
|
+
}
|
|
2374
|
+
return {
|
|
2375
|
+
ref1Raw,
|
|
2376
|
+
ref2Raw,
|
|
2377
|
+
entries
|
|
2378
|
+
};
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
// arr[0] is the operator; operands are arr[1..arr.length-1]
|
|
2382
|
+
function emitShortCircuit(arr, jumpOp, markerOp, state) {
|
|
2383
|
+
const {
|
|
2384
|
+
bytecode
|
|
2385
|
+
} = state;
|
|
2386
|
+
const jumpSlots = [];
|
|
2387
|
+
const last = arr.length - 1;
|
|
2388
|
+
for (let i = 1; i < last; i++) {
|
|
2389
|
+
emitExpression(arr[i], state);
|
|
2390
|
+
bytecode.push(jumpOp);
|
|
2391
|
+
jumpSlots.push(bytecode.length);
|
|
2392
|
+
bytecode.push(0); // placeholder — backpatched below
|
|
2393
|
+
bytecode.push(OP_POP); // discard result before evaluating next operand
|
|
2394
|
+
}
|
|
2395
|
+
emitExpression(arr[last], state);
|
|
2396
|
+
const end = bytecode.length;
|
|
2397
|
+
for (const slot of jumpSlots) {
|
|
2398
|
+
bytecode[slot] = end - slot - 1;
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// Emit marker so the simplify interpreter knows the operator and operand count
|
|
2402
|
+
bytecode.push(markerOp, last);
|
|
2403
|
+
}
|
|
2404
|
+
function emitExpression(raw, state) {
|
|
2405
|
+
const {
|
|
2406
|
+
bytecode,
|
|
2407
|
+
maps
|
|
2408
|
+
} = state;
|
|
2409
|
+
if (!Array.isArray(raw)) {
|
|
2410
|
+
emitOperand(raw, state);
|
|
2411
|
+
return;
|
|
2412
|
+
}
|
|
2413
|
+
const arr = raw;
|
|
2414
|
+
const operator = arr[0];
|
|
2415
|
+
const nOperands = arr.length - 1;
|
|
2416
|
+
if (typeof operator !== 'string') {
|
|
2417
|
+
emitOperand(raw, state);
|
|
2418
|
+
return;
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2421
|
+
// ---------------------------------------------------------------------------
|
|
2422
|
+
// Logical — short-circuit with jump instructions
|
|
2423
|
+
// ---------------------------------------------------------------------------
|
|
2424
|
+
if (operator === maps.andOp) {
|
|
2425
|
+
emitShortCircuit(arr, OP_JUMP_IF_FALSE, OP_AND, state);
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
if (operator === maps.orOp) {
|
|
2429
|
+
const orAnd2 = detectOrAndIn2Pattern(arr, state);
|
|
2430
|
+
if (orAnd2 !== null) {
|
|
2431
|
+
const {
|
|
2432
|
+
ref1Raw,
|
|
2433
|
+
ref2Raw,
|
|
2434
|
+
entries
|
|
2435
|
+
} = orAnd2;
|
|
2436
|
+
const {
|
|
2437
|
+
bytecode
|
|
2438
|
+
} = state;
|
|
2439
|
+
const ref1Idx = internRef(ref1Raw, state);
|
|
2440
|
+
const ref2Idx = internRef(ref2Raw, state);
|
|
2441
|
+
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M v0 setBIdx0 v1 setBIdx1 ... vM-1 setBIdxM-1
|
|
2442
|
+
// M is the number of distinct setA values across all branches (after inverted-index merge).
|
|
2443
|
+
bytecode.push(OP_OR_AND_IN_CONST_2, ref1Idx, ref2Idx, entries.length);
|
|
2444
|
+
for (const [aVal, mergedSetBIdx] of entries) {
|
|
2445
|
+
bytecode.push(aVal, mergedSetBIdx);
|
|
2446
|
+
}
|
|
2447
|
+
return;
|
|
2448
|
+
}
|
|
2449
|
+
emitShortCircuit(arr, OP_JUMP_IF_TRUE, OP_OR, state);
|
|
2450
|
+
return;
|
|
2451
|
+
}
|
|
2452
|
+
if (operator === maps.norOp) {
|
|
2453
|
+
// NOR = NOT OR: emit as OR with short-circuit, then negate
|
|
2454
|
+
emitShortCircuit(arr, OP_JUMP_IF_TRUE, OP_NOR, state);
|
|
2455
|
+
bytecode.push(OP_NOT);
|
|
2456
|
+
return;
|
|
2457
|
+
}
|
|
2458
|
+
if (operator === maps.notOp) {
|
|
2459
|
+
emitExpression(arr[1], state);
|
|
2460
|
+
bytecode.push(OP_NOT);
|
|
2461
|
+
return;
|
|
2462
|
+
}
|
|
2463
|
+
if (operator === maps.xorOp) {
|
|
2464
|
+
// XOR is associative: chain binary XOR operations
|
|
2465
|
+
// (A XOR B) XOR C XOR D ...
|
|
2466
|
+
emitExpression(arr[1], state);
|
|
2467
|
+
for (let i = 2; i <= nOperands; i++) {
|
|
2468
|
+
emitExpression(arr[i], state);
|
|
2469
|
+
bytecode.push(OP_XOR);
|
|
2470
|
+
}
|
|
2471
|
+
return;
|
|
2472
|
+
}
|
|
2473
|
+
|
|
2474
|
+
// ---------------------------------------------------------------------------
|
|
2475
|
+
// Comparison — binary (left, right)
|
|
2476
|
+
// ---------------------------------------------------------------------------
|
|
2477
|
+
|
|
2478
|
+
// IN / NOT_IN: optimized paths when one operand is a collection
|
|
2479
|
+
if (operator === maps.inOp || operator === maps.notInOp) {
|
|
2480
|
+
const left = arr[1];
|
|
2481
|
+
const right = arr[2];
|
|
2482
|
+
if (Array.isArray(left) && !Array.isArray(right)) {
|
|
2483
|
+
const leftArr = left;
|
|
2484
|
+
const leftHasDynamic = leftArr.some(v => typeof v === 'string' && state.opts.referencePredicate(v));
|
|
2485
|
+
const constOp = operator === maps.inOp ? OP_IN_CONST : OP_NOT_IN_CONST;
|
|
2486
|
+
const collectionOp = operator === maps.inOp ? OP_IN_COLLECTION : OP_NOT_IN_COLLECTION;
|
|
2487
|
+
if (!leftHasDynamic) {
|
|
2488
|
+
// fully static collection on left — intern as const, Set-lookup the scalar
|
|
2489
|
+
emitExpression(right, state);
|
|
2490
|
+
const opcodePos = bytecode.length;
|
|
2491
|
+
bytecode.push(constOp, internConst(leftArr, state));
|
|
2492
|
+
state.directionEntries.push({
|
|
2493
|
+
pos: opcodePos,
|
|
2494
|
+
dir: 0
|
|
2495
|
+
});
|
|
2496
|
+
} else if (isPureRefCollection(leftArr, state.opts) && !(typeof right === 'string' && state.opts.referencePredicate(right))) {
|
|
2497
|
+
// pure-ref collection on left, concrete scalar on right — inline ref scan, no stack alloc
|
|
2498
|
+
const scanOp = operator === maps.inOp ? OP_IN_SCAN_REFS_CONST : OP_NOT_IN_SCAN_REFS_CONST;
|
|
2499
|
+
const constIdx = internConst([right], state);
|
|
2500
|
+
const opcodePos = bytecode.length;
|
|
2501
|
+
const refIdxs = [];
|
|
2502
|
+
bytecode.push(scanOp, leftArr.length);
|
|
2503
|
+
for (const item of leftArr) {
|
|
2504
|
+
const refIdx = internRef(item, state);
|
|
2505
|
+
bytecode.push(refIdx);
|
|
2506
|
+
refIdxs.push(refIdx);
|
|
2507
|
+
}
|
|
2508
|
+
bytecode.push(constIdx);
|
|
2509
|
+
state.directionEntries.push({
|
|
2510
|
+
pos: opcodePos,
|
|
2511
|
+
dir: 0
|
|
2512
|
+
});
|
|
2513
|
+
state.overlapRefsEntries.push({
|
|
2514
|
+
pos: opcodePos,
|
|
2515
|
+
refIdxs
|
|
2516
|
+
});
|
|
2517
|
+
} else {
|
|
2518
|
+
// mixed dynamic collection on left — inline stack scan
|
|
2519
|
+
for (const item of leftArr) {
|
|
2520
|
+
emitOperand(item, state);
|
|
2521
|
+
}
|
|
2522
|
+
emitExpression(right, state);
|
|
2523
|
+
const opcodePos = bytecode.length;
|
|
2524
|
+
bytecode.push(collectionOp, leftArr.length);
|
|
2525
|
+
state.directionEntries.push({
|
|
2526
|
+
pos: opcodePos,
|
|
2527
|
+
dir: 0
|
|
2528
|
+
});
|
|
2529
|
+
}
|
|
2530
|
+
return;
|
|
2531
|
+
}
|
|
2532
|
+
if (Array.isArray(right) && !Array.isArray(left)) {
|
|
2533
|
+
const rightArr = right;
|
|
2534
|
+
const rightHasDynamic = rightArr.some(v => typeof v === 'string' && state.opts.referencePredicate(v));
|
|
2535
|
+
const constOp = operator === maps.inOp ? OP_IN_CONST : OP_NOT_IN_CONST;
|
|
2536
|
+
const collectionOp = operator === maps.inOp ? OP_IN_COLLECTION : OP_NOT_IN_COLLECTION;
|
|
2537
|
+
if (!rightHasDynamic) {
|
|
2538
|
+
// fully static collection on right — intern as const, Set-lookup the scalar
|
|
2539
|
+
emitExpression(left, state);
|
|
2540
|
+
const opcodePos = bytecode.length;
|
|
2541
|
+
bytecode.push(constOp, internConst(rightArr, state));
|
|
2542
|
+
state.directionEntries.push({
|
|
2543
|
+
pos: opcodePos,
|
|
2544
|
+
dir: 1
|
|
2545
|
+
});
|
|
2546
|
+
} else if (isPureRefCollection(rightArr, state.opts) && !(typeof left === 'string' && state.opts.referencePredicate(left))) {
|
|
2547
|
+
// pure-ref collection on right, concrete scalar on left — inline ref scan, no stack alloc
|
|
2548
|
+
const scanOp = operator === maps.inOp ? OP_IN_SCAN_REFS_CONST : OP_NOT_IN_SCAN_REFS_CONST;
|
|
2549
|
+
const constIdx = internConst([left], state);
|
|
2550
|
+
const opcodePos = bytecode.length;
|
|
2551
|
+
const refIdxs = [];
|
|
2552
|
+
bytecode.push(scanOp, rightArr.length);
|
|
2553
|
+
for (const item of rightArr) {
|
|
2554
|
+
const refIdx = internRef(item, state);
|
|
2555
|
+
bytecode.push(refIdx);
|
|
2556
|
+
refIdxs.push(refIdx);
|
|
2557
|
+
}
|
|
2558
|
+
bytecode.push(constIdx);
|
|
2559
|
+
state.directionEntries.push({
|
|
2560
|
+
pos: opcodePos,
|
|
2561
|
+
dir: 1
|
|
2562
|
+
});
|
|
2563
|
+
state.overlapRefsEntries.push({
|
|
2564
|
+
pos: opcodePos,
|
|
2565
|
+
refIdxs
|
|
2566
|
+
});
|
|
2567
|
+
} else {
|
|
2568
|
+
// mixed dynamic collection on right — inline stack scan
|
|
2569
|
+
for (const item of rightArr) {
|
|
2570
|
+
emitOperand(item, state);
|
|
2571
|
+
}
|
|
2572
|
+
emitExpression(left, state);
|
|
2573
|
+
const opcodePos = bytecode.length;
|
|
2574
|
+
bytecode.push(collectionOp, rightArr.length);
|
|
2575
|
+
state.directionEntries.push({
|
|
2576
|
+
pos: opcodePos,
|
|
2577
|
+
dir: 1
|
|
2578
|
+
});
|
|
2579
|
+
}
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
// both sides are dynamic (refs) — fall through to generic OP_IN / OP_NOT_IN
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
// OVERLAP: if exactly one operand is a static collection, emit optimized path
|
|
2586
|
+
if (operator === maps.overlapOp) {
|
|
2587
|
+
const left = arr[1];
|
|
2588
|
+
const right = arr[2];
|
|
2589
|
+
if (isStaticCollection(left, state.opts) && !isStaticCollection(right, state.opts)) {
|
|
2590
|
+
const constIdx = internConst(left, state);
|
|
2591
|
+
if (isPureRefCollection(right, state.opts)) {
|
|
2592
|
+
// dynamic side is all refs — inline ref indices, resolve+check at runtime, no stack alloc
|
|
2593
|
+
const opcodePos = bytecode.length;
|
|
2594
|
+
bytecode.push(OP_OVERLAP_SCAN_REFS_CONST, right.length);
|
|
2595
|
+
const refIdxs = [];
|
|
2596
|
+
for (const item of right) {
|
|
2597
|
+
if (typeof item !== 'string') {
|
|
2598
|
+
throw new Error('OVERLAP: expected string ref in pure-ref collection');
|
|
2599
|
+
}
|
|
2600
|
+
const refIdx = internRef(item, state);
|
|
2601
|
+
bytecode.push(refIdx);
|
|
2602
|
+
refIdxs.push(refIdx);
|
|
2603
|
+
}
|
|
2604
|
+
bytecode.push(constIdx);
|
|
2605
|
+
state.directionEntries.push({
|
|
2606
|
+
pos: opcodePos,
|
|
2607
|
+
dir: 0
|
|
2608
|
+
});
|
|
2609
|
+
state.overlapRefsEntries.push({
|
|
2610
|
+
pos: opcodePos,
|
|
2611
|
+
refIdxs
|
|
2612
|
+
});
|
|
2613
|
+
} else {
|
|
2614
|
+
emitExpression(right, state);
|
|
2615
|
+
const opcodePos = bytecode.length;
|
|
2616
|
+
bytecode.push(OP_OVERLAP_CONST, constIdx);
|
|
2617
|
+
state.directionEntries.push({
|
|
2618
|
+
pos: opcodePos,
|
|
2619
|
+
dir: 0
|
|
2620
|
+
});
|
|
2621
|
+
}
|
|
2622
|
+
return;
|
|
2623
|
+
}
|
|
2624
|
+
if (isStaticCollection(right, state.opts) && !isStaticCollection(left, state.opts)) {
|
|
2625
|
+
const constIdx = internConst(right, state);
|
|
2626
|
+
if (isPureRefCollection(left, state.opts)) {
|
|
2627
|
+
// dynamic side is all refs — inline ref indices, resolve+check at runtime, no stack alloc
|
|
2628
|
+
const opcodePos = bytecode.length;
|
|
2629
|
+
bytecode.push(OP_OVERLAP_SCAN_REFS_CONST, left.length);
|
|
2630
|
+
const refIdxs = [];
|
|
2631
|
+
for (const item of left) {
|
|
2632
|
+
if (typeof item !== 'string') {
|
|
2633
|
+
throw new Error('OVERLAP: expected string ref in pure-ref collection');
|
|
2634
|
+
}
|
|
2635
|
+
const refIdx = internRef(item, state);
|
|
2636
|
+
bytecode.push(refIdx);
|
|
2637
|
+
refIdxs.push(refIdx);
|
|
2638
|
+
}
|
|
2639
|
+
bytecode.push(constIdx);
|
|
2640
|
+
state.directionEntries.push({
|
|
2641
|
+
pos: opcodePos,
|
|
2642
|
+
dir: 1
|
|
2643
|
+
});
|
|
2644
|
+
state.overlapRefsEntries.push({
|
|
2645
|
+
pos: opcodePos,
|
|
2646
|
+
refIdxs
|
|
2647
|
+
});
|
|
2648
|
+
} else {
|
|
2649
|
+
emitExpression(left, state);
|
|
2650
|
+
const opcodePos = bytecode.length;
|
|
2651
|
+
bytecode.push(OP_OVERLAP_CONST, constIdx);
|
|
2652
|
+
state.directionEntries.push({
|
|
2653
|
+
pos: opcodePos,
|
|
2654
|
+
dir: 1
|
|
2655
|
+
});
|
|
2656
|
+
}
|
|
2657
|
+
return;
|
|
2658
|
+
}
|
|
2659
|
+
// Both static or both dynamic: fall through to generic OP_OVERLAP
|
|
2660
|
+
}
|
|
2661
|
+
if (operator in maps.binary) {
|
|
2662
|
+
emitExpression(arr[1], state);
|
|
2663
|
+
emitExpression(arr[2], state);
|
|
2664
|
+
bytecode.push(maps.binary[operator]);
|
|
2665
|
+
return;
|
|
2666
|
+
}
|
|
2667
|
+
|
|
2668
|
+
// ---------------------------------------------------------------------------
|
|
2669
|
+
// Unary comparison
|
|
2670
|
+
// ---------------------------------------------------------------------------
|
|
2671
|
+
if (operator === maps.presentOp) {
|
|
2672
|
+
emitExpression(arr[1], state);
|
|
2673
|
+
bytecode.push(OP_PRESENT);
|
|
2674
|
+
return;
|
|
2675
|
+
}
|
|
2676
|
+
if (operator === maps.undefinedOp) {
|
|
2677
|
+
emitExpression(arr[1], state);
|
|
2678
|
+
bytecode.push(OP_UNDEFINED);
|
|
2679
|
+
return;
|
|
2680
|
+
}
|
|
2681
|
+
|
|
2682
|
+
// ---------------------------------------------------------------------------
|
|
2683
|
+
// Arithmetic — N operands
|
|
2684
|
+
// ---------------------------------------------------------------------------
|
|
2685
|
+
if (operator in maps.arithmetic) {
|
|
2686
|
+
for (let i = 1; i <= nOperands; i++) {
|
|
2687
|
+
emitExpression(arr[i], state);
|
|
2688
|
+
}
|
|
2689
|
+
bytecode.push(maps.arithmetic[operator], nOperands);
|
|
2690
|
+
return;
|
|
2691
|
+
}
|
|
2692
|
+
|
|
2693
|
+
// ---------------------------------------------------------------------------
|
|
2694
|
+
// Fallback: treat as collection (array of values/refs)
|
|
2695
|
+
// ---------------------------------------------------------------------------
|
|
2696
|
+
emitOperand(raw, state);
|
|
2697
|
+
}
|
|
2698
|
+
/**
|
|
2699
|
+
* Compile a raw ExpressionInput into bytecode.
|
|
2700
|
+
* The result should be cached and reused across evaluate() calls.
|
|
2701
|
+
*/
|
|
2702
|
+
function compile(raw, opts) {
|
|
2703
|
+
const maps = buildOperatorMaps(opts);
|
|
2704
|
+
const state = {
|
|
2705
|
+
bytecode: [],
|
|
2706
|
+
refs: [],
|
|
2707
|
+
refIndex: new Map(),
|
|
2708
|
+
refRawKeys: [],
|
|
2709
|
+
refKeys: [],
|
|
2710
|
+
opts,
|
|
2711
|
+
maps,
|
|
2712
|
+
collectionCse: new Map(),
|
|
2713
|
+
numLocals: 0,
|
|
2714
|
+
consts: [],
|
|
2715
|
+
constIndex: new Map(),
|
|
2716
|
+
overlapRefsEntries: [],
|
|
2717
|
+
directionEntries: []
|
|
2718
|
+
};
|
|
2719
|
+
emitExpression(raw, state);
|
|
2720
|
+
|
|
2721
|
+
// Build reverse map: opcode → operator string for residual reconstruction
|
|
2722
|
+
const opNames = {};
|
|
2723
|
+
for (const [str, code] of Object.entries(maps.binary)) {
|
|
2724
|
+
opNames[code] = str;
|
|
2725
|
+
}
|
|
2726
|
+
for (const [str, code] of Object.entries(maps.arithmetic)) {
|
|
2727
|
+
opNames[code] = str;
|
|
2728
|
+
}
|
|
2729
|
+
opNames[OP_NOT] = maps.notOp;
|
|
2730
|
+
opNames[OP_AND] = maps.andOp;
|
|
2731
|
+
opNames[OP_OR] = maps.orOp;
|
|
2732
|
+
opNames[OP_NOR] = maps.norOp;
|
|
2733
|
+
opNames[OP_XOR] = maps.xorOp;
|
|
2734
|
+
opNames[OP_PRESENT] = maps.presentOp;
|
|
2735
|
+
opNames[OP_UNDEFINED] = maps.undefinedOp;
|
|
2736
|
+
opNames[OP_IN_COLLECTION] = maps.inOp;
|
|
2737
|
+
opNames[OP_NOT_IN_COLLECTION] = maps.notInOp;
|
|
2738
|
+
opNames[OP_IN_CONST] = maps.inOp;
|
|
2739
|
+
opNames[OP_NOT_IN_CONST] = maps.notInOp;
|
|
2740
|
+
opNames[OP_OVERLAP_CONST] = maps.overlapOp;
|
|
2741
|
+
opNames[OP_OVERLAP_SCAN_REFS_CONST] = maps.overlapOp;
|
|
2742
|
+
opNames[OP_IN_SCAN_REFS_CONST] = maps.inOp;
|
|
2743
|
+
opNames[OP_NOT_IN_SCAN_REFS_CONST] = maps.notInOp;
|
|
2744
|
+
|
|
2745
|
+
// Pre-build residual Input[] arrays for OP_OVERLAP_SCAN_REFS_CONST — eliminates per-call allocation in the simplifier
|
|
2746
|
+
const overlapRefsResiduals = state.overlapRefsEntries.map(({
|
|
2747
|
+
pos,
|
|
2748
|
+
refIdxs
|
|
2749
|
+
}) => [pos, refIdxs.map(idx => state.refKeys[idx])]);
|
|
2750
|
+
|
|
2751
|
+
// Build direction entries as serializable tuple array
|
|
2752
|
+
const directionMap = state.directionEntries.map(({
|
|
2753
|
+
pos,
|
|
2754
|
+
dir
|
|
2755
|
+
}) => [pos, dir]);
|
|
2756
|
+
return {
|
|
2757
|
+
bytecode: state.bytecode,
|
|
2758
|
+
refs: state.refs,
|
|
2759
|
+
numLocals: state.numLocals,
|
|
2760
|
+
consts: state.consts,
|
|
2761
|
+
opNames,
|
|
2762
|
+
refKeys: state.refKeys,
|
|
2763
|
+
refRawKeys: state.refRawKeys,
|
|
2764
|
+
overlapRefsResiduals,
|
|
2765
|
+
directionMap,
|
|
2766
|
+
refFirstCtxKeys: state.refs.map(getFirstCtxKey)
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
|
|
2770
|
+
const getNumDecimals = num => {
|
|
2771
|
+
const numberSplit = num.toString().split('.');
|
|
2772
|
+
return numberSplit.length == 2 ? numberSplit[1].length : 0;
|
|
2773
|
+
};
|
|
2774
|
+
const operateWithExpectedDecimals = operation => (first, second) => {
|
|
2775
|
+
const numDecimals1 = getNumDecimals(first);
|
|
2776
|
+
const numDecimals2 = getNumDecimals(second);
|
|
2777
|
+
const maxDecimals = operation === 'multiply' ? numDecimals1 + numDecimals2 : numDecimals1 > numDecimals2 ? numDecimals1 : numDecimals2;
|
|
2778
|
+
return operation === 'sum' ? Number((first + second).toFixed(maxDecimals)) : operation === 'subtract' ? Number((first - second).toFixed(maxDecimals)) : Number((first * second).toFixed(maxDecimals));
|
|
2779
|
+
};
|
|
2780
|
+
|
|
2781
|
+
/**
|
|
2782
|
+
* Bytecode interpreter.
|
|
2783
|
+
*
|
|
2784
|
+
* Executes a compiled bytecode array against a context object.
|
|
2785
|
+
* Zero allocations in the hot path: uses a pre-allocated stack
|
|
2786
|
+
* and operates on the flat bytecode array directly.
|
|
2787
|
+
*/
|
|
2788
|
+
|
|
2789
|
+
|
|
2790
|
+
// Read a numeric value from a bytecode slot — opcodes and index operands are always numbers.
|
|
2791
|
+
// Throws if the slot contains a non-number (guards against compiler bugs).
|
|
2792
|
+
function numAt$1(v) {
|
|
2793
|
+
if (typeof v !== 'number') {
|
|
2794
|
+
throw new Error(`bytecode integrity error: expected number, got ${typeof v}`);
|
|
2795
|
+
}
|
|
2796
|
+
return v;
|
|
2797
|
+
}
|
|
2798
|
+
const addDecimals$1 = operateWithExpectedDecimals('sum');
|
|
2799
|
+
const subtractDecimals$1 = operateWithExpectedDecimals('subtract');
|
|
2800
|
+
const multiplyDecimals$1 = operateWithExpectedDecimals('multiply');
|
|
2801
|
+
const divideDecimals$1 = (a, b) => a / b;
|
|
2802
|
+
|
|
2803
|
+
// Pre-allocated stack — safe because evaluation is synchronous and non-reentrant
|
|
2804
|
+
const MAX_STACK$1 = 512;
|
|
2805
|
+
const stack$1 = new Array(MAX_STACK$1);
|
|
2806
|
+
let stackTop$1 = -1;
|
|
2807
|
+
|
|
2808
|
+
// Pre-allocated locals for CSE collection caching — grown on demand
|
|
2809
|
+
const MAX_LOCALS$1 = 64;
|
|
2810
|
+
const locals$1 = new Array(MAX_LOCALS$1);
|
|
2811
|
+
function relationalCompare$1(left, right, op) {
|
|
2812
|
+
if (isNumber(left) && isNumber(right)) {
|
|
2813
|
+
if (op === OP_GT) {
|
|
2814
|
+
return left > right;
|
|
2815
|
+
}
|
|
2816
|
+
if (op === OP_GE) {
|
|
2817
|
+
return left >= right;
|
|
2818
|
+
}
|
|
2819
|
+
if (op === OP_LT) {
|
|
2820
|
+
return left < right;
|
|
2821
|
+
}
|
|
2822
|
+
return left <= right;
|
|
2823
|
+
}
|
|
2824
|
+
const ld = toDateNumber(left);
|
|
2825
|
+
const rd = toDateNumber(right);
|
|
2826
|
+
if (!isNaN(ld) && !isNaN(rd)) {
|
|
2827
|
+
if (op === OP_GT) {
|
|
2828
|
+
return ld > rd;
|
|
2829
|
+
}
|
|
2830
|
+
if (op === OP_GE) {
|
|
2831
|
+
return ld >= rd;
|
|
2832
|
+
}
|
|
2833
|
+
if (op === OP_LT) {
|
|
2834
|
+
return ld < rd;
|
|
2835
|
+
}
|
|
2836
|
+
return ld <= rd;
|
|
2837
|
+
}
|
|
2838
|
+
return false;
|
|
2839
|
+
}
|
|
2840
|
+
function dateArithmeticReduce$1(values, op) {
|
|
2841
|
+
const [date, ...durations] = values;
|
|
2842
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
2843
|
+
}
|
|
2844
|
+
function arithmeticReduce$1(values, op) {
|
|
2845
|
+
if (op === OP_SUM) {
|
|
2846
|
+
return values.reduce(addDecimals$1);
|
|
2847
|
+
}
|
|
2848
|
+
if (op === OP_SUBTRACT) {
|
|
2849
|
+
return values.reduce(subtractDecimals$1);
|
|
2850
|
+
}
|
|
2851
|
+
if (op === OP_MULTIPLY) {
|
|
2852
|
+
return values.reduce(multiplyDecimals$1);
|
|
2853
|
+
}
|
|
2854
|
+
return values.reduce(divideDecimals$1);
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2857
|
+
/**
|
|
2858
|
+
* Execute compiled bytecode against a context.
|
|
2859
|
+
* Returns the top-of-stack value when execution completes.
|
|
2860
|
+
*/
|
|
2861
|
+
// Per-compiled-expression Set cache — one WeakMap lookup per interpret() call,
|
|
2862
|
+
// then all Set lookups use plain array indexing by constIdx.
|
|
2863
|
+
const constSetsCache = new WeakMap();
|
|
2864
|
+
function interpret(compiled, ctx) {
|
|
2865
|
+
const {
|
|
2866
|
+
bytecode,
|
|
2867
|
+
refs,
|
|
2868
|
+
consts
|
|
2869
|
+
} = compiled;
|
|
2870
|
+
let constSets = constSetsCache.get(compiled);
|
|
2871
|
+
if (constSets === undefined) {
|
|
2872
|
+
constSets = new Array(consts.length);
|
|
2873
|
+
for (let j = 0; j < consts.length; j++) {
|
|
2874
|
+
constSets[j] = new Set(consts[j]);
|
|
2875
|
+
}
|
|
2876
|
+
constSetsCache.set(compiled, constSets);
|
|
2877
|
+
}
|
|
2878
|
+
const len = bytecode.length;
|
|
2879
|
+
stackTop$1 = -1;
|
|
2880
|
+
let i = 0;
|
|
2881
|
+
while (i < len) {
|
|
2882
|
+
const op = numAt$1(bytecode[i]);
|
|
2883
|
+
switch (op) {
|
|
2884
|
+
// -----------------------------------------------------------------------
|
|
2885
|
+
// Operands
|
|
2886
|
+
// -----------------------------------------------------------------------
|
|
2887
|
+
case OP_PUSH_VALUE:
|
|
2888
|
+
stack$1[++stackTop$1] = bytecode[++i];
|
|
2889
|
+
break;
|
|
2890
|
+
case OP_PUSH_REF_KEY:
|
|
2891
|
+
stack$1[++stackTop$1] = ctx[asKeyRef(refs[numAt$1(bytecode[++i])])];
|
|
2892
|
+
break;
|
|
2893
|
+
case OP_PUSH_REF_KEYS:
|
|
2894
|
+
stack$1[++stackTop$1] = resolveKeys(asKeysRef(refs[numAt$1(bytecode[++i])]), ctx);
|
|
2895
|
+
break;
|
|
2896
|
+
case OP_PUSH_REF_TOKENS:
|
|
2897
|
+
{
|
|
2898
|
+
const ref = asFullRef(refs[numAt$1(bytecode[++i])]);
|
|
2899
|
+
stack$1[++stackTop$1] = resolveTokens(ref.tokens ?? [], ref.t, ctx);
|
|
2900
|
+
break;
|
|
2901
|
+
}
|
|
2902
|
+
case OP_PUSH_REF_DYNAMIC:
|
|
2903
|
+
{
|
|
2904
|
+
const ref = asFullRef(refs[numAt$1(bytecode[++i])]);
|
|
2905
|
+
stack$1[++stackTop$1] = resolveDynamic(ref.k, ref.t, ctx);
|
|
2906
|
+
break;
|
|
2907
|
+
}
|
|
2908
|
+
case OP_MAKE_COLLECTION:
|
|
2909
|
+
{
|
|
2910
|
+
const n = numAt$1(bytecode[++i]);
|
|
2911
|
+
const arr = new Array(n);
|
|
2912
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
2913
|
+
arr[j] = stack$1[stackTop$1--];
|
|
2914
|
+
}
|
|
2915
|
+
stack$1[++stackTop$1] = arr;
|
|
2916
|
+
break;
|
|
2917
|
+
}
|
|
2918
|
+
case OP_PUSH_CONST:
|
|
2919
|
+
stack$1[++stackTop$1] = consts[numAt$1(bytecode[++i])];
|
|
2920
|
+
break;
|
|
2921
|
+
case OP_OVERLAP_CONST:
|
|
2922
|
+
{
|
|
2923
|
+
const constIdx = numAt$1(bytecode[++i]);
|
|
2924
|
+
const constArr = consts[constIdx];
|
|
2925
|
+
const dynamic = stack$1[stackTop$1--];
|
|
2926
|
+
if (!Array.isArray(dynamic)) {
|
|
2927
|
+
stack$1[++stackTop$1] = false;
|
|
2928
|
+
break;
|
|
2929
|
+
}
|
|
2930
|
+
const dLen = dynamic.length;
|
|
2931
|
+
const cLen = constArr.length;
|
|
2932
|
+
if (cLen === 0 && dLen === 0) {
|
|
2933
|
+
stack$1[++stackTop$1] = true;
|
|
2934
|
+
break;
|
|
2935
|
+
}
|
|
2936
|
+
let found = false;
|
|
2937
|
+
if (cLen === 1) {
|
|
2938
|
+
const target = constArr[0];
|
|
2939
|
+
for (let j = 0; j < dLen; j++) {
|
|
2940
|
+
if (dynamic[j] === target) {
|
|
2941
|
+
found = true;
|
|
2942
|
+
break;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
} else {
|
|
2946
|
+
const s = constSets[constIdx];
|
|
2947
|
+
for (let j = 0; j < dLen; j++) {
|
|
2948
|
+
if (s.has(dynamic[j])) {
|
|
2949
|
+
found = true;
|
|
2950
|
+
break;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
}
|
|
2954
|
+
stack$1[++stackTop$1] = found;
|
|
2955
|
+
break;
|
|
2956
|
+
}
|
|
2957
|
+
case OP_STORE_LOCAL:
|
|
2958
|
+
// Peek top (don't pop) and store into locals slot
|
|
2959
|
+
locals$1[numAt$1(bytecode[++i])] = stack$1[stackTop$1];
|
|
2960
|
+
break;
|
|
2961
|
+
case OP_LOAD_LOCAL:
|
|
2962
|
+
stack$1[++stackTop$1] = locals$1[numAt$1(bytecode[++i])];
|
|
2963
|
+
break;
|
|
2964
|
+
|
|
2965
|
+
// -----------------------------------------------------------------------
|
|
2966
|
+
// Equality
|
|
2967
|
+
// -----------------------------------------------------------------------
|
|
2968
|
+
case OP_EQ:
|
|
2969
|
+
{
|
|
2970
|
+
const right = stack$1[stackTop$1--];
|
|
2971
|
+
const left = stack$1[stackTop$1--];
|
|
2972
|
+
stack$1[++stackTop$1] = left === right;
|
|
2973
|
+
break;
|
|
2974
|
+
}
|
|
2975
|
+
case OP_NE:
|
|
2976
|
+
{
|
|
2977
|
+
const right = stack$1[stackTop$1--];
|
|
2978
|
+
const left = stack$1[stackTop$1--];
|
|
2979
|
+
stack$1[++stackTop$1] = left !== right;
|
|
2980
|
+
break;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
// -----------------------------------------------------------------------
|
|
2984
|
+
// Relational
|
|
2985
|
+
// -----------------------------------------------------------------------
|
|
2986
|
+
case OP_GT:
|
|
2987
|
+
case OP_GE:
|
|
2988
|
+
case OP_LT:
|
|
2989
|
+
case OP_LE:
|
|
2990
|
+
{
|
|
2991
|
+
const right = stack$1[stackTop$1--];
|
|
2992
|
+
const left = stack$1[stackTop$1--];
|
|
2993
|
+
stack$1[++stackTop$1] = relationalCompare$1(left, right, op);
|
|
2994
|
+
break;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
// -----------------------------------------------------------------------
|
|
2998
|
+
// Containment
|
|
2999
|
+
// -----------------------------------------------------------------------
|
|
3000
|
+
case OP_IN:
|
|
3001
|
+
{
|
|
3002
|
+
const right = stack$1[stackTop$1--];
|
|
3003
|
+
const left = stack$1[stackTop$1--];
|
|
3004
|
+
if (left === null || left === undefined || right === null || right === undefined) {
|
|
3005
|
+
stack$1[++stackTop$1] = false;
|
|
3006
|
+
break;
|
|
3007
|
+
}
|
|
3008
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
3009
|
+
throw new Error('IN: both operands are arrays');
|
|
3010
|
+
}
|
|
3011
|
+
if (!Array.isArray(left) && !Array.isArray(right)) {
|
|
3012
|
+
throw new Error('IN: neither operand is an array');
|
|
3013
|
+
}
|
|
3014
|
+
if (Array.isArray(left)) {
|
|
3015
|
+
stack$1[++stackTop$1] = left.indexOf(right) > -1;
|
|
3016
|
+
} else if (Array.isArray(right)) {
|
|
3017
|
+
stack$1[++stackTop$1] = right.indexOf(left) > -1;
|
|
3018
|
+
}
|
|
3019
|
+
break;
|
|
3020
|
+
}
|
|
3021
|
+
case OP_NOT_IN:
|
|
3022
|
+
{
|
|
3023
|
+
const right = stack$1[stackTop$1--];
|
|
3024
|
+
const left = stack$1[stackTop$1--];
|
|
3025
|
+
if (left === null || left === undefined || right === null || right === undefined) {
|
|
3026
|
+
stack$1[++stackTop$1] = true;
|
|
3027
|
+
break;
|
|
3028
|
+
}
|
|
3029
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
3030
|
+
throw new Error('NOT IN: both operands are arrays');
|
|
3031
|
+
}
|
|
3032
|
+
if (!Array.isArray(left) && !Array.isArray(right)) {
|
|
3033
|
+
throw new Error('NOT IN: neither operand is an array');
|
|
3034
|
+
}
|
|
3035
|
+
if (Array.isArray(left)) {
|
|
3036
|
+
stack$1[++stackTop$1] = left.indexOf(right) === -1;
|
|
3037
|
+
} else if (Array.isArray(right)) {
|
|
3038
|
+
stack$1[++stackTop$1] = right.indexOf(left) === -1;
|
|
3039
|
+
}
|
|
3040
|
+
break;
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
// Inline collection scan — no array allocation.
|
|
3044
|
+
// Stack layout: [item0, item1, ..., itemN-1, scalar]
|
|
3045
|
+
// The compiler always emits collection items first, scalar last.
|
|
3046
|
+
case OP_IN_COLLECTION:
|
|
3047
|
+
{
|
|
3048
|
+
const n = numAt$1(bytecode[++i]);
|
|
3049
|
+
const scalar = stack$1[stackTop$1--];
|
|
3050
|
+
let found = false;
|
|
3051
|
+
if (scalar !== null && scalar !== undefined) {
|
|
3052
|
+
for (let j = 0; j < n; j++) {
|
|
3053
|
+
if (stack$1[stackTop$1 - j] === scalar) {
|
|
3054
|
+
found = true;
|
|
3055
|
+
break;
|
|
3056
|
+
}
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
stackTop$1 -= n;
|
|
3060
|
+
stack$1[++stackTop$1] = found;
|
|
3061
|
+
break;
|
|
3062
|
+
}
|
|
3063
|
+
case OP_NOT_IN_COLLECTION:
|
|
3064
|
+
{
|
|
3065
|
+
const n = numAt$1(bytecode[++i]);
|
|
3066
|
+
const scalar = stack$1[stackTop$1--];
|
|
3067
|
+
let found = false;
|
|
3068
|
+
if (scalar !== null && scalar !== undefined) {
|
|
3069
|
+
for (let j = 0; j < n; j++) {
|
|
3070
|
+
if (stack$1[stackTop$1 - j] === scalar) {
|
|
3071
|
+
found = true;
|
|
3072
|
+
break;
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
stackTop$1 -= n;
|
|
3077
|
+
stack$1[++stackTop$1] = !found;
|
|
3078
|
+
break;
|
|
3079
|
+
}
|
|
3080
|
+
case OP_IN_CONST:
|
|
3081
|
+
{
|
|
3082
|
+
const constIdx = numAt$1(bytecode[++i]);
|
|
3083
|
+
const scalar = stack$1[stackTop$1--];
|
|
3084
|
+
if (scalar === null || scalar === undefined) {
|
|
3085
|
+
stack$1[++stackTop$1] = false;
|
|
3086
|
+
break;
|
|
3087
|
+
}
|
|
3088
|
+
stack$1[++stackTop$1] = constSets[constIdx].has(scalar);
|
|
3089
|
+
break;
|
|
3090
|
+
}
|
|
3091
|
+
case OP_NOT_IN_CONST:
|
|
3092
|
+
{
|
|
3093
|
+
const constIdx = numAt$1(bytecode[++i]);
|
|
3094
|
+
const scalar = stack$1[stackTop$1--];
|
|
3095
|
+
if (scalar === null || scalar === undefined) {
|
|
3096
|
+
stack$1[++stackTop$1] = true;
|
|
3097
|
+
break;
|
|
3098
|
+
}
|
|
3099
|
+
stack$1[++stackTop$1] = !constSets[constIdx].has(scalar);
|
|
3100
|
+
break;
|
|
3101
|
+
}
|
|
3102
|
+
case OP_OVERLAP_SCAN_REFS_CONST:
|
|
3103
|
+
{
|
|
3104
|
+
// bytecode layout: n, ref0, ref1, ..., refN-1, constIdx
|
|
3105
|
+
const n = numAt$1(bytecode[++i]);
|
|
3106
|
+
const refStart = i + 1;
|
|
3107
|
+
i += n;
|
|
3108
|
+
const constIdx = numAt$1(bytecode[++i]);
|
|
3109
|
+
const constArr = consts[constIdx];
|
|
3110
|
+
const cLen = constArr.length;
|
|
3111
|
+
let found = false;
|
|
3112
|
+
if (cLen === 0) {
|
|
3113
|
+
// n > 0 here, so cannot be two empty arrays
|
|
3114
|
+
found = false;
|
|
3115
|
+
} else if (cLen === 1) {
|
|
3116
|
+
const target = constArr[0];
|
|
3117
|
+
for (let j = 0; j < n; j++) {
|
|
3118
|
+
const v = resolveCompactRef(refs[numAt$1(bytecode[refStart + j])], ctx);
|
|
3119
|
+
if (v === target) {
|
|
3120
|
+
found = true;
|
|
3121
|
+
break;
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
} else {
|
|
3125
|
+
const s = constSets[constIdx];
|
|
3126
|
+
const hasUndefined = s.has(undefined);
|
|
3127
|
+
const hasNull = s.has(null);
|
|
3128
|
+
for (let j = 0; j < n; j++) {
|
|
3129
|
+
const v = resolveCompactRef(refs[numAt$1(bytecode[refStart + j])], ctx);
|
|
3130
|
+
if (v === undefined) {
|
|
3131
|
+
if (hasUndefined) {
|
|
3132
|
+
found = true;
|
|
3133
|
+
break;
|
|
3134
|
+
}
|
|
3135
|
+
continue;
|
|
3136
|
+
}
|
|
3137
|
+
if (v === null) {
|
|
3138
|
+
if (hasNull) {
|
|
3139
|
+
found = true;
|
|
3140
|
+
break;
|
|
3141
|
+
}
|
|
3142
|
+
continue;
|
|
3143
|
+
}
|
|
3144
|
+
if (s.has(v)) {
|
|
3145
|
+
found = true;
|
|
3146
|
+
break;
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
}
|
|
3150
|
+
stack$1[++stackTop$1] = found;
|
|
3151
|
+
break;
|
|
3152
|
+
}
|
|
3153
|
+
case OP_IN_SCAN_REFS_CONST:
|
|
3154
|
+
case OP_NOT_IN_SCAN_REFS_CONST:
|
|
3155
|
+
{
|
|
3156
|
+
// bytecode layout: n, ref0, ref1, ..., refN-1, constIdx (always 1-element const)
|
|
3157
|
+
const n = numAt$1(bytecode[++i]);
|
|
3158
|
+
const refStart = i + 1;
|
|
3159
|
+
i += n;
|
|
3160
|
+
const constIdx = numAt$1(bytecode[++i]);
|
|
3161
|
+
const target = consts[constIdx][0]; // always a 1-element set: the scalar operand
|
|
3162
|
+
let found = false;
|
|
3163
|
+
if (target !== null && target !== undefined) {
|
|
3164
|
+
for (let j = 0; j < n; j++) {
|
|
3165
|
+
const v = resolveCompactRef(refs[numAt$1(bytecode[refStart + j])], ctx);
|
|
3166
|
+
if (v === target) {
|
|
3167
|
+
found = true;
|
|
3168
|
+
break;
|
|
3169
|
+
}
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
stack$1[++stackTop$1] = op === OP_IN_SCAN_REFS_CONST ? found : !found;
|
|
3173
|
+
break;
|
|
3174
|
+
}
|
|
3175
|
+
case OP_OR_AND_IN_CONST_2:
|
|
3176
|
+
{
|
|
3177
|
+
// bytecode layout: ref1Idx, ref2Idx, M, v0, setBIdx0, v1, setBIdx1, ..., vM-1, setBIdxM-1
|
|
3178
|
+
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3179
|
+
const ref1Idx = numAt$1(bytecode[++i]);
|
|
3180
|
+
const ref2Idx = numAt$1(bytecode[++i]);
|
|
3181
|
+
const m = numAt$1(bytecode[++i]);
|
|
3182
|
+
const entriesStart = i + 1;
|
|
3183
|
+
i += m * 2;
|
|
3184
|
+
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
3185
|
+
const v2 = resolveCompactRef(refs[ref2Idx], ctx);
|
|
3186
|
+
let found = false;
|
|
3187
|
+
if (v1 !== undefined && v1 !== null && v2 !== undefined && v2 !== null) {
|
|
3188
|
+
for (let j = 0; j < m; j++) {
|
|
3189
|
+
if (bytecode[entriesStart + j * 2] === v1) {
|
|
3190
|
+
found = constSets[numAt$1(bytecode[entriesStart + j * 2 + 1])].has(v2);
|
|
3191
|
+
break;
|
|
3192
|
+
}
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
stack$1[++stackTop$1] = found;
|
|
3196
|
+
break;
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
// -----------------------------------------------------------------------
|
|
3200
|
+
// String
|
|
3201
|
+
// -----------------------------------------------------------------------
|
|
3202
|
+
case OP_PREFIX:
|
|
3203
|
+
{
|
|
3204
|
+
const right = stack$1[stackTop$1--];
|
|
3205
|
+
const left = stack$1[stackTop$1--];
|
|
3206
|
+
stack$1[++stackTop$1] = isString(left) && isString(right) ? right.startsWith(left) : false;
|
|
3207
|
+
break;
|
|
3208
|
+
}
|
|
3209
|
+
case OP_SUFFIX:
|
|
3210
|
+
{
|
|
3211
|
+
const right = stack$1[stackTop$1--];
|
|
3212
|
+
const left = stack$1[stackTop$1--];
|
|
3213
|
+
stack$1[++stackTop$1] = isString(left) && isString(right) ? left.endsWith(right) : false;
|
|
3214
|
+
break;
|
|
3215
|
+
}
|
|
3216
|
+
|
|
3217
|
+
// -----------------------------------------------------------------------
|
|
3218
|
+
// Array
|
|
3219
|
+
// -----------------------------------------------------------------------
|
|
3220
|
+
case OP_OVERLAP:
|
|
3221
|
+
{
|
|
3222
|
+
const right = stack$1[stackTop$1--];
|
|
3223
|
+
const left = stack$1[stackTop$1--];
|
|
3224
|
+
if (left === null || left === undefined || right === null || right === undefined) {
|
|
3225
|
+
stack$1[++stackTop$1] = false;
|
|
3226
|
+
break;
|
|
3227
|
+
}
|
|
3228
|
+
if (!Array.isArray(left) || !Array.isArray(right)) {
|
|
3229
|
+
throw new Error('OVERLAP: both operands must be arrays');
|
|
3230
|
+
}
|
|
3231
|
+
if (left.length === 0 && right.length === 0) {
|
|
3232
|
+
stack$1[++stackTop$1] = true;
|
|
3233
|
+
break;
|
|
3234
|
+
}
|
|
3235
|
+
stack$1[++stackTop$1] = left.some(el => right.includes(el));
|
|
3236
|
+
break;
|
|
3237
|
+
}
|
|
3238
|
+
|
|
3239
|
+
// -----------------------------------------------------------------------
|
|
3240
|
+
// Presence
|
|
3241
|
+
// -----------------------------------------------------------------------
|
|
3242
|
+
case OP_PRESENT:
|
|
3243
|
+
stack$1[stackTop$1] = stack$1[stackTop$1] !== undefined && stack$1[stackTop$1] !== null;
|
|
3244
|
+
break;
|
|
3245
|
+
case OP_UNDEFINED:
|
|
3246
|
+
stack$1[stackTop$1] = stack$1[stackTop$1] === undefined;
|
|
3247
|
+
break;
|
|
3248
|
+
|
|
3249
|
+
// -----------------------------------------------------------------------
|
|
3250
|
+
// Arithmetic
|
|
3251
|
+
// -----------------------------------------------------------------------
|
|
3252
|
+
case OP_SUM:
|
|
3253
|
+
case OP_SUBTRACT:
|
|
3254
|
+
case OP_MULTIPLY:
|
|
3255
|
+
case OP_DIVIDE:
|
|
3256
|
+
{
|
|
3257
|
+
const n = numAt$1(bytecode[++i]);
|
|
3258
|
+
// Fast path for the common 2-operand case — no array allocation
|
|
3259
|
+
if (n === 2) {
|
|
3260
|
+
const b = stack$1[stackTop$1--];
|
|
3261
|
+
const a = stack$1[stackTop$1--];
|
|
3262
|
+
if (a === null || a === undefined || b === null || b === undefined) {
|
|
3263
|
+
stack$1[++stackTop$1] = false;
|
|
3264
|
+
break;
|
|
3265
|
+
}
|
|
3266
|
+
const isDateArithmetic = !isNaN(toDateNumber(a));
|
|
3267
|
+
if (!isNumber(a) || !isNumber(b)) {
|
|
3268
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
3269
|
+
if (isDateArithmetic) {
|
|
3270
|
+
const duration = toDateDuration(b);
|
|
3271
|
+
if (!duration) {
|
|
3272
|
+
throw new Error(`arithmetic operand is not a date duration: ${b}`);
|
|
3273
|
+
}
|
|
3274
|
+
} else {
|
|
3275
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3276
|
+
}
|
|
3277
|
+
} else {
|
|
3278
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(a) ? a : b}`);
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
if (op === OP_SUM) {
|
|
3282
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? addDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'sum'));
|
|
3283
|
+
} else if (op === OP_SUBTRACT) {
|
|
3284
|
+
stack$1[++stackTop$1] = !isDateArithmetic && isNumber(a) && isNumber(b) ? subtractDecimals$1(a, b) : formatDateNumber(mutateDateWithDuration(toDateNumber(a), toDateDuration(b), 'subtract'));
|
|
3285
|
+
} else if (isNumber(a) && isNumber(b)) {
|
|
3286
|
+
if (op === OP_MULTIPLY) {
|
|
3287
|
+
stack$1[++stackTop$1] = multiplyDecimals$1(a, b);
|
|
3288
|
+
} else {
|
|
3289
|
+
stack$1[++stackTop$1] = divideDecimals$1(a, b);
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
break;
|
|
3293
|
+
}
|
|
3294
|
+
const values = new Array(n);
|
|
3295
|
+
let hasNull = false;
|
|
3296
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[stackTop$1 - n + 1])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3297
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
3298
|
+
const v = stack$1[stackTop$1--];
|
|
3299
|
+
if (v === null || v === undefined) {
|
|
3300
|
+
hasNull = true;
|
|
3301
|
+
break;
|
|
3302
|
+
}
|
|
3303
|
+
if (!isNumber(v)) {
|
|
3304
|
+
if (isDateArithmetic) {
|
|
3305
|
+
const duration = toDateDuration(v);
|
|
3306
|
+
if (!duration && j > 0) {
|
|
3307
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
3308
|
+
}
|
|
3309
|
+
} else {
|
|
3310
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
if (isNumber(v) || isString(v)) {
|
|
3314
|
+
values[j] = v;
|
|
3315
|
+
}
|
|
3316
|
+
}
|
|
3317
|
+
stack$1[++stackTop$1] = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce$1(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce$1(values, op) : false;
|
|
3318
|
+
break;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
3321
|
+
// -----------------------------------------------------------------------
|
|
3322
|
+
// Logical
|
|
3323
|
+
// -----------------------------------------------------------------------
|
|
3324
|
+
case OP_NOT:
|
|
3325
|
+
{
|
|
3326
|
+
const val = stack$1[stackTop$1];
|
|
3327
|
+
if (typeof val !== 'boolean') {
|
|
3328
|
+
throw new Error('NOT: operand must be boolean');
|
|
3329
|
+
}
|
|
3330
|
+
stack$1[stackTop$1] = !val;
|
|
3331
|
+
break;
|
|
3332
|
+
}
|
|
3333
|
+
case OP_XOR:
|
|
3334
|
+
{
|
|
3335
|
+
const b = stack$1[stackTop$1--];
|
|
3336
|
+
const a = stack$1[stackTop$1--];
|
|
3337
|
+
if (typeof a !== 'boolean' || typeof b !== 'boolean') {
|
|
3338
|
+
throw new Error('XOR: operands must be boolean');
|
|
3339
|
+
}
|
|
3340
|
+
stack$1[++stackTop$1] = (a || b) && !(a && b);
|
|
3341
|
+
break;
|
|
3342
|
+
}
|
|
3343
|
+
case OP_JUMP_IF_FALSE:
|
|
3344
|
+
{
|
|
3345
|
+
const offset = numAt$1(bytecode[++i]);
|
|
3346
|
+
if (stack$1[stackTop$1] === false) {
|
|
3347
|
+
i += offset;
|
|
3348
|
+
}
|
|
3349
|
+
break;
|
|
3350
|
+
}
|
|
3351
|
+
case OP_JUMP_IF_TRUE:
|
|
3352
|
+
{
|
|
3353
|
+
const offset = numAt$1(bytecode[++i]);
|
|
3354
|
+
if (stack$1[stackTop$1] === true) {
|
|
3355
|
+
i += offset;
|
|
3356
|
+
}
|
|
3357
|
+
break;
|
|
3358
|
+
}
|
|
3359
|
+
case OP_POP:
|
|
3360
|
+
stackTop$1--;
|
|
3361
|
+
break;
|
|
3362
|
+
case OP_AND:
|
|
3363
|
+
case OP_OR:
|
|
3364
|
+
case OP_NOR:
|
|
3365
|
+
i++; // skip operand count
|
|
3366
|
+
break;
|
|
3367
|
+
}
|
|
3368
|
+
i++;
|
|
3369
|
+
}
|
|
3370
|
+
return stack$1[stackTop$1];
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
/**
|
|
3374
|
+
* Bytecode simplify interpreter.
|
|
3375
|
+
*
|
|
3376
|
+
* Runs the same bytecode as the evaluate interpreter but supports partial
|
|
3377
|
+
* evaluation. Stack slots hold either a Result (fully resolved) or an Input
|
|
3378
|
+
* fragment (residual — a ref string or a sub-expression array). When an
|
|
3379
|
+
* operator receives any residual operand it reconstructs the sub-expression
|
|
3380
|
+
* as an Input array instead of computing a value.
|
|
3381
|
+
*
|
|
3382
|
+
* Short-circuit logic still applies: AND with a false short-circuits even if
|
|
3383
|
+
* other operands are unknown; OR with a true short-circuits likewise.
|
|
3384
|
+
*/
|
|
3385
|
+
|
|
3386
|
+
|
|
3387
|
+
// Detect Infinity and NaN — these should not be used as concrete values in
|
|
3388
|
+
// comparisons when the other operand is residual, matching the OOP simplifier's
|
|
3389
|
+
// isInfinite guard in isSimplifiedArithmeticExpression.
|
|
3390
|
+
function isUnusableResult(v) {
|
|
3391
|
+
return typeof v === 'number' && !isFinite(v);
|
|
3392
|
+
}
|
|
3393
|
+
const addDecimals = operateWithExpectedDecimals('sum');
|
|
3394
|
+
const subtractDecimals = operateWithExpectedDecimals('subtract');
|
|
3395
|
+
const multiplyDecimals = operateWithExpectedDecimals('multiply');
|
|
3396
|
+
const divideDecimals = (a, b) => a / b;
|
|
3397
|
+
|
|
3398
|
+
// WeakMap Set cache for OP_OVERLAP_CONST, OP_OVERLAP_SCAN_REFS_CONST, and OP_IN_CONST —
|
|
3399
|
+
// same pattern as the evaluate interpreter.
|
|
3400
|
+
// Keyed on the const array identity so Sets are released when the CompiledExpression is GC'd.
|
|
3401
|
+
const overlapSetCache = new WeakMap();
|
|
3402
|
+
|
|
3403
|
+
// Per-compiled-expression Map caches — built lazily from the serializable tuple arrays on the
|
|
3404
|
+
// CompiledExpression. Same WeakMap pattern as constSetsCache in the evaluate interpreter so
|
|
3405
|
+
// the Maps are released when the CompiledExpression is GC'd.
|
|
3406
|
+
const overlapRefsResidualsCache = new WeakMap();
|
|
3407
|
+
const directionMapCache = new WeakMap();
|
|
3408
|
+
|
|
3409
|
+
// Read a numeric operand from a bytecode slot — opcodes and index operands are always numbers.
|
|
3410
|
+
// Throws if the slot contains a non-number (guards against compiler bugs).
|
|
3411
|
+
function numAt(v) {
|
|
3412
|
+
if (typeof v !== 'number') {
|
|
3413
|
+
throw new Error(`bytecode integrity error: expected number, got ${typeof v}`);
|
|
3414
|
+
}
|
|
3415
|
+
return v;
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3418
|
+
// Read a literal value from a bytecode slot — stored literals are string|number|boolean|null.
|
|
3419
|
+
// Throws if the slot contains undefined, an array, or an object (guards against compiler bugs).
|
|
3420
|
+
// Returns string|number|boolean|null, which is a subtype of Input.
|
|
3421
|
+
function literalAt(v) {
|
|
3422
|
+
if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean' || v === null) {
|
|
3423
|
+
return v;
|
|
3424
|
+
}
|
|
3425
|
+
throw new Error(`bytecode integrity error: expected literal, got ${typeof v}`);
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
// Retrieve a required entry from a map — throws if the key is missing.
|
|
3429
|
+
// Eliminates the Map.get() undefined return in contexts where the entry is compiler-guaranteed.
|
|
3430
|
+
function requireMapEntry(map, key) {
|
|
3431
|
+
const v = map.get(key);
|
|
3432
|
+
if (v === undefined) {
|
|
3433
|
+
throw new Error(`bytecode integrity error: missing required map entry for key ${String(key)}`);
|
|
3434
|
+
}
|
|
3435
|
+
return v;
|
|
3436
|
+
}
|
|
3437
|
+
|
|
3438
|
+
// A resolved ref carries its computed value and original serialized form.
|
|
3439
|
+
// Used so that when a comparison partially resolves, we reconstruct using
|
|
3440
|
+
// the original ref key (e.g. '$a') rather than the resolved value (e.g. 10).
|
|
3441
|
+
// Numeric tag _r used for all three wrapper types so V8 can use hidden-class
|
|
3442
|
+
// fixed-offset reads instead of the slower `'key' in obj` property-existence check.
|
|
3443
|
+
|
|
3444
|
+
// A residual sub-expression — a reconstructed Input fragment.
|
|
3445
|
+
// Wrapped in an object so we can distinguish it from concrete Result[] arrays.
|
|
3446
|
+
|
|
3447
|
+
// Accumulated XOR state during chained binary XOR evaluation.
|
|
3448
|
+
// Tracks residual operands and how many concrete-true values were seen.
|
|
3449
|
+
|
|
3450
|
+
// Union of all slot object wrapper types (distinguishable via _r discriminant).
|
|
3451
|
+
|
|
3452
|
+
// Arrays on the stack carry no _r property. Declaring _r?: undefined here exposes
|
|
3453
|
+
// the discriminant on the full "non-null object Slot" union so that needsReconstruct
|
|
3454
|
+
// and slotVal can read ._r directly — without Array.isArray — in their hot paths.
|
|
3455
|
+
// For arrays, ._r resolves to undefined, which naturally fails the === 2/3/1 checks.
|
|
3456
|
+
|
|
3457
|
+
// A stack slot is a concrete value (primitives, arrays, SlotObject wrappers).
|
|
3458
|
+
// Record<string,unknown> values can be pushed directly from OP_PUSH_VALUE
|
|
3459
|
+
// (object literals in expressions), and are returned as-is by slotVal.
|
|
3460
|
+
|
|
3461
|
+
// Type guard: checks if a slot is a SlotObject (has _r as an own property).
|
|
3462
|
+
// This distinguishes SlotObject types from plain Record<string,unknown> objects.
|
|
3463
|
+
function isSlotObject(v) {
|
|
3464
|
+
return typeof v === 'object' && v !== null && Object.prototype.hasOwnProperty.call(v, '_r');
|
|
3465
|
+
}
|
|
3466
|
+
function isXorState(v) {
|
|
3467
|
+
return isSlotObject(v) && v._r === 3;
|
|
3468
|
+
}
|
|
3469
|
+
function isResidual(v) {
|
|
3470
|
+
return isSlotObject(v) && v._r === 2;
|
|
3471
|
+
}
|
|
3472
|
+
function isDivByZeroMarker(v) {
|
|
3473
|
+
return isSlotObject(v) && v._r === 4;
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// True if the slot needs to be reconstructed rather than computed.
|
|
3477
|
+
// Hot path — booleans/numbers/strings short-circuit at the first typeof check.
|
|
3478
|
+
// Unknown refs are always wrapped in Residual (_r === 2), never pushed as raw strings,
|
|
3479
|
+
// so no reference-predicate check is needed here.
|
|
3480
|
+
// No Array.isArray needed: arrays have _r === undefined, failing the 2/3/4 check.
|
|
3481
|
+
function needsReconstruct(v) {
|
|
3482
|
+
if (typeof v !== 'object' || v === null) {
|
|
3483
|
+
return false;
|
|
3484
|
+
}
|
|
3485
|
+
// v is ArraySlot | SlotObject | Record<string, unknown>; _r is undefined|1|2|3|4
|
|
3486
|
+
return v._r === 2 || v._r === 3 || v._r === 4;
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3489
|
+
// Extract the concrete Result from a slot (unwrap Resolved if needed).
|
|
3490
|
+
// Hot path — no Array.isArray needed: for non-Resolved objects (arrays, Residual,
|
|
3491
|
+
// XorState, DivByZeroMarker), the _r === 1 check is false and they
|
|
3492
|
+
// fall through to the primitive branch or return the value as appropriate.
|
|
3493
|
+
function slotVal(v) {
|
|
3494
|
+
if (typeof v !== 'object' || v === null) {
|
|
3495
|
+
return v; // primitives: boolean, number, string, null, undefined
|
|
3496
|
+
}
|
|
3497
|
+
// v is ArraySlot | SlotObject | Record<string,unknown>
|
|
3498
|
+
if (isSlotObject(v)) {
|
|
3499
|
+
// v is a SlotObject — discriminate via _r
|
|
3500
|
+
if (v._r === 1) {
|
|
3501
|
+
return v.val; // Resolved — unwrap the concrete value
|
|
3502
|
+
}
|
|
3503
|
+
if (v._r === 4) {
|
|
3504
|
+
// DivByZeroMarker — unwrap as Infinity
|
|
3505
|
+
return v._val;
|
|
3506
|
+
}
|
|
3507
|
+
// v._r is 2 or 3 (Residual/XorState) — no concrete value to extract
|
|
3508
|
+
return undefined;
|
|
3509
|
+
}
|
|
3510
|
+
// v is ArraySlot or Record<string,unknown> — both are valid Results
|
|
3511
|
+
if (Array.isArray(v)) {
|
|
3512
|
+
return v; // ArraySlot — the array is itself a Result
|
|
3513
|
+
}
|
|
3514
|
+
return v; // Plain object literal — Record<string,unknown> is a valid Result
|
|
3515
|
+
}
|
|
3516
|
+
|
|
3517
|
+
// Module-level op name references for XorState finalization.
|
|
3518
|
+
// Set at the start of each interpretSimplify call.
|
|
3519
|
+
let _xorOpName = 'XOR';
|
|
3520
|
+
let _notOpName = 'NOT';
|
|
3521
|
+
let _norOpName = 'NOR';
|
|
3522
|
+
|
|
3523
|
+
// Type guard: checks whether a Result value is also a valid Input.
|
|
3524
|
+
// Input = string | number | boolean | null | Input[] | Record<string, unknown>.
|
|
3525
|
+
function isInput(v) {
|
|
3526
|
+
if (v === undefined) {
|
|
3527
|
+
return false;
|
|
3528
|
+
}
|
|
3529
|
+
if (v === null) {
|
|
3530
|
+
return true;
|
|
3531
|
+
}
|
|
3532
|
+
if (Array.isArray(v)) {
|
|
3533
|
+
return v.every(isInput);
|
|
3534
|
+
}
|
|
3535
|
+
if (typeof v === 'object') {
|
|
3536
|
+
return Object.values(v).every(isInput);
|
|
3537
|
+
}
|
|
3538
|
+
return true;
|
|
3539
|
+
}
|
|
3540
|
+
|
|
3541
|
+
// Extract the serialized Input form of a slot for residual reconstruction.
|
|
3542
|
+
function slotSrc(v) {
|
|
3543
|
+
if (typeof v !== 'object' || v === null) {
|
|
3544
|
+
// v is a primitive: undefined | null | string | number | boolean
|
|
3545
|
+
// In residual paths this should always be a valid Input — guard against null/undefined.
|
|
3546
|
+
if (!isInput(v)) {
|
|
3547
|
+
throw new Error(`slotSrc: invariant violated — non-Input value in residual path: ${typeof v}`);
|
|
3548
|
+
}
|
|
3549
|
+
return v; // v narrowed to Input by the type guard above
|
|
3550
|
+
}
|
|
3551
|
+
// v is ArraySlot | SlotObject | Record<string,unknown>
|
|
3552
|
+
if (Array.isArray(v)) {
|
|
3553
|
+
// v is ArraySlot — a concrete collection used as an operand
|
|
3554
|
+
if (!isInput(v)) {
|
|
3555
|
+
throw new Error(`slotSrc: invariant violated — non-Input array in residual path`);
|
|
3556
|
+
}
|
|
3557
|
+
return v;
|
|
3558
|
+
}
|
|
3559
|
+
// v is a plain object — use isSlotObject to distinguish SlotObject types
|
|
3560
|
+
// (which have _r as an own property) from plain Record<string,unknown> objects.
|
|
3561
|
+
if (isSlotObject(v)) {
|
|
3562
|
+
// v is a SlotObject — discriminate via _r
|
|
3563
|
+
if (v._r === 3) {
|
|
3564
|
+
// XorState — finalize accumulated XOR state into an expression
|
|
3565
|
+
const {
|
|
3566
|
+
xorResiduals: residuals,
|
|
3567
|
+
xorTrueCount: trueCount
|
|
3568
|
+
} = v;
|
|
3569
|
+
// "One-hot" XOR: if more than one true was seen, result is false
|
|
3570
|
+
if (trueCount > 1) {
|
|
3571
|
+
return false;
|
|
3572
|
+
}
|
|
3573
|
+
const effectiveTrueCount = trueCount % 2;
|
|
3574
|
+
if (residuals.length === 1) {
|
|
3575
|
+
return effectiveTrueCount === 1 ? [_notOpName, residuals[0]] : residuals[0];
|
|
3576
|
+
}
|
|
3577
|
+
return effectiveTrueCount === 1 ? [_norOpName, ...residuals] : [_xorOpName, ...residuals];
|
|
3578
|
+
}
|
|
3579
|
+
if (v._r === 2) {
|
|
3580
|
+
// Residual — return the reconstructed expression
|
|
3581
|
+
return v.expr;
|
|
3582
|
+
}
|
|
3583
|
+
if (v._r === 4) {
|
|
3584
|
+
// DivByZeroMarker — reconstruct as a DIVIDE expression
|
|
3585
|
+
return ['/', v.left, v.right];
|
|
3586
|
+
}
|
|
3587
|
+
if (v._r === 1) {
|
|
3588
|
+
// Resolved — return original ref key
|
|
3589
|
+
return v.src;
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
// Plain object literal — return as-is (valid Input)
|
|
3593
|
+
return v;
|
|
3594
|
+
}
|
|
3595
|
+
|
|
3596
|
+
// Wrap a reconstructed expression as a Residual slot.
|
|
3597
|
+
// expr should be an Input array (sub-expression) like ['EQ', '$ref', 5].
|
|
3598
|
+
function makeResidual(expr) {
|
|
3599
|
+
return {
|
|
3600
|
+
_r: 2,
|
|
3601
|
+
expr
|
|
3602
|
+
};
|
|
3603
|
+
}
|
|
3604
|
+
|
|
3605
|
+
// Create a Resolved slot wrapping a known ref value.
|
|
3606
|
+
function makeResolved(val, src) {
|
|
3607
|
+
return {
|
|
3608
|
+
_r: 1,
|
|
3609
|
+
val,
|
|
3610
|
+
src
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3614
|
+
// Create a XorState slot accumulating XOR operands.
|
|
3615
|
+
function makeXorState(xorResiduals, xorTrueCount) {
|
|
3616
|
+
return {
|
|
3617
|
+
_r: 3,
|
|
3618
|
+
xorResiduals,
|
|
3619
|
+
xorTrueCount
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
// Pre-allocated stack — same as evaluate interpreter
|
|
3624
|
+
const MAX_STACK = 512;
|
|
3625
|
+
const stack = new Array(MAX_STACK);
|
|
3626
|
+
let stackTop = -1;
|
|
3627
|
+
|
|
3628
|
+
// Pre-allocated locals for CSE collection caching
|
|
3629
|
+
const MAX_LOCALS = 64;
|
|
3630
|
+
const locals = new Array(MAX_LOCALS);
|
|
3631
|
+
|
|
3632
|
+
// Pre-resolved ref cache — populated lazily per interpretSimplify call.
|
|
3633
|
+
// Indexed by ref index; avoids repeated resolveCompactRef() calls when the same
|
|
3634
|
+
// ref appears in multiple opcodes (e.g. multiple OVERLAP_SCAN_REFS_CONST in one expression).
|
|
3635
|
+
// resolvedRefDirty[i] = true means resolvedRefCache[i] holds a valid (possibly undefined) result.
|
|
3636
|
+
const MAX_REFS = 512;
|
|
3637
|
+
const resolvedRefCache = new Array(MAX_REFS);
|
|
3638
|
+
const resolvedRefDirty = new Array(MAX_REFS).fill(false);
|
|
3639
|
+
// Tracks which indices were populated this call so we can reset only those.
|
|
3640
|
+
const resolvedRefUsed = new Array(MAX_REFS);
|
|
3641
|
+
let resolvedRefUsedCount = 0;
|
|
3642
|
+
|
|
3643
|
+
// Residual spill buffer: when OP_POP discards a residual/unknown-ref slot that
|
|
3644
|
+
// is part of a short-circuit AND/OR sequence, we save it here so OP_AND/OR/NOR
|
|
3645
|
+
// can include it in the reconstructed expression.
|
|
3646
|
+
const spillBuf = new Array(MAX_STACK);
|
|
3647
|
+
let spillTop = -1;
|
|
3648
|
+
// Track the last jump opcode type to detect transitions between short-circuit
|
|
3649
|
+
// sequences. Different jump opcodes (41=JUMP_IF_FALSE vs 42=JUMP_IF_TRUE) indicate
|
|
3650
|
+
// different short-circuit sequences (AND vs OR/NOR).
|
|
3651
|
+
let lastJumpOp = 0;
|
|
3652
|
+
function relationalCompare(left, right, op) {
|
|
3653
|
+
if (isNumber(left) && isNumber(right)) {
|
|
3654
|
+
if (op === OP_GT) {
|
|
3655
|
+
return left > right;
|
|
3656
|
+
}
|
|
3657
|
+
if (op === OP_GE) {
|
|
3658
|
+
return left >= right;
|
|
3659
|
+
}
|
|
3660
|
+
if (op === OP_LT) {
|
|
3661
|
+
return left < right;
|
|
3662
|
+
}
|
|
3663
|
+
return left <= right;
|
|
3664
|
+
}
|
|
3665
|
+
const ld = toDateNumber(left);
|
|
3666
|
+
const rd = toDateNumber(right);
|
|
3667
|
+
if (!isNaN(ld) && !isNaN(rd)) {
|
|
3668
|
+
if (op === OP_GT) {
|
|
3669
|
+
return ld > rd;
|
|
3670
|
+
}
|
|
3671
|
+
if (op === OP_GE) {
|
|
3672
|
+
return ld >= rd;
|
|
3673
|
+
}
|
|
3674
|
+
if (op === OP_LT) {
|
|
3675
|
+
return ld < rd;
|
|
3676
|
+
}
|
|
3677
|
+
return ld <= rd;
|
|
3678
|
+
}
|
|
3679
|
+
return false;
|
|
3680
|
+
}
|
|
3681
|
+
function arithmeticReduce(values, op) {
|
|
3682
|
+
if (op === OP_SUM) {
|
|
3683
|
+
return values.reduce(addDecimals);
|
|
3684
|
+
}
|
|
3685
|
+
if (op === OP_SUBTRACT) {
|
|
3686
|
+
return values.reduce(subtractDecimals);
|
|
3687
|
+
}
|
|
3688
|
+
if (op === OP_MULTIPLY) {
|
|
3689
|
+
return values.reduce(multiplyDecimals);
|
|
3690
|
+
}
|
|
3691
|
+
return values.reduce(divideDecimals);
|
|
3692
|
+
}
|
|
3693
|
+
function dateArithmeticReduce(values, op) {
|
|
3694
|
+
const [date, ...durations] = values;
|
|
3695
|
+
return formatDateNumber(durations.reduce((mutated, duration) => mutateDateWithDuration(mutated, toDateDuration(duration), op === OP_SUM ? 'sum' : 'subtract'), toDateNumber(date)));
|
|
3696
|
+
}
|
|
3697
|
+
function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
3698
|
+
const {
|
|
3699
|
+
bytecode,
|
|
3700
|
+
refs,
|
|
3701
|
+
opNames,
|
|
3702
|
+
refKeys,
|
|
3703
|
+
refRawKeys,
|
|
3704
|
+
refFirstCtxKeys
|
|
3705
|
+
} = compiled;
|
|
3706
|
+
stackTop = -1;
|
|
3707
|
+
spillTop = -1;
|
|
3708
|
+
lastJumpOp = 0;
|
|
3709
|
+
let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
|
|
3710
|
+
if (overlapRefsResiduals === undefined) {
|
|
3711
|
+
overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
|
|
3712
|
+
overlapRefsResidualsCache.set(compiled, overlapRefsResiduals);
|
|
3713
|
+
}
|
|
3714
|
+
let directionMap = directionMapCache.get(compiled);
|
|
3715
|
+
if (directionMap === undefined) {
|
|
3716
|
+
directionMap = new Map(compiled.directionMap);
|
|
3717
|
+
directionMapCache.set(compiled, directionMap);
|
|
3718
|
+
}
|
|
3719
|
+
|
|
3720
|
+
// Reset the ref cache (only clear slots used in the previous call)
|
|
3721
|
+
for (let r = 0; r < resolvedRefUsedCount; r++) {
|
|
3722
|
+
resolvedRefDirty[resolvedRefUsed[r]] = false;
|
|
3723
|
+
}
|
|
3724
|
+
resolvedRefUsedCount = 0;
|
|
3725
|
+
|
|
3726
|
+
// Set module-level op names for XorState finalization
|
|
3727
|
+
_xorOpName = opNames[OP_XOR] ?? 'XOR';
|
|
3728
|
+
_notOpName = opNames[OP_NOT] ?? 'NOT';
|
|
3729
|
+
_norOpName = opNames[OP_NOR] ?? 'NOR';
|
|
3730
|
+
let i = 0;
|
|
3731
|
+
const len = bytecode.length;
|
|
3732
|
+
|
|
3733
|
+
// Normalise key sets for O(1) lookup
|
|
3734
|
+
const strictSet = strictKeys instanceof Set ? strictKeys : strictKeys ? new Set(strictKeys) : undefined;
|
|
3735
|
+
const optionalSet = optionalKeys instanceof Set ? optionalKeys : optionalKeys ? new Set(optionalKeys) : undefined;
|
|
3736
|
+
while (i < len) {
|
|
3737
|
+
const op = numAt(bytecode[i++]);
|
|
3738
|
+
switch (op) {
|
|
3739
|
+
// ---------------------------------------------------------------------
|
|
3740
|
+
// Push
|
|
3741
|
+
// ---------------------------------------------------------------------
|
|
3742
|
+
case OP_PUSH_VALUE:
|
|
3743
|
+
{
|
|
3744
|
+
const lit = bytecode[i++];
|
|
3745
|
+
// OP_PUSH_VALUE stores parsed Input literals, including object literals.
|
|
3746
|
+
// No guard needed — the compiler guarantees valid Input values.
|
|
3747
|
+
stack[++stackTop] = lit;
|
|
3748
|
+
break;
|
|
3749
|
+
}
|
|
3750
|
+
case OP_PUSH_REF_KEY:
|
|
3751
|
+
case OP_PUSH_REF_KEYS:
|
|
3752
|
+
case OP_PUSH_REF_TOKENS:
|
|
3753
|
+
case OP_PUSH_REF_DYNAMIC:
|
|
3754
|
+
{
|
|
3755
|
+
const idx = numAt(bytecode[i++]);
|
|
3756
|
+
const rawKey = refRawKeys[idx];
|
|
3757
|
+
let val;
|
|
3758
|
+
if (resolvedRefDirty[idx]) {
|
|
3759
|
+
val = resolvedRefCache[idx];
|
|
3760
|
+
} else {
|
|
3761
|
+
val = resolveCompactRef(refs[idx], ctx);
|
|
3762
|
+
resolvedRefCache[idx] = val;
|
|
3763
|
+
resolvedRefDirty[idx] = true;
|
|
3764
|
+
resolvedRefUsed[resolvedRefUsedCount++] = idx;
|
|
3765
|
+
}
|
|
3766
|
+
if (val !== undefined) {
|
|
3767
|
+
// Ref is in context — always resolve (strictKeys/optionalKeys only affect absent keys)
|
|
3768
|
+
stack[++stackTop] = makeResolved(val, refKeys[idx]);
|
|
3769
|
+
break;
|
|
3770
|
+
}
|
|
3771
|
+
|
|
3772
|
+
// Ref is absent from context.
|
|
3773
|
+
// Check if it should be treated as a concrete undefined (evaluated as undefined)
|
|
3774
|
+
// or as a residual expression (preserved for later simplification).
|
|
3775
|
+
if (strictSet?.has(rawKey)) {
|
|
3776
|
+
// strictKeys: force-evaluate as undefined (not a residual)
|
|
3777
|
+
stack[++stackTop] = undefined;
|
|
3778
|
+
break;
|
|
3779
|
+
}
|
|
3780
|
+
if (optionalSet && !optionalSet.has(rawKey)) {
|
|
3781
|
+
// Key not in optionalKeys: treat as definitely-present but absent → undefined
|
|
3782
|
+
stack[++stackTop] = undefined;
|
|
3783
|
+
break;
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
// First-key heuristic: match OOP Reference.simplify() behavior.
|
|
3787
|
+
// If the top-level context key exists, the ref's parent object is known — treat the
|
|
3788
|
+
// (absent) sub-field as concrete undefined rather than preserving it as an unknown.
|
|
3789
|
+
// This only applies to multi-key refs (refFirstCtxKeys[idx] is undefined for single-key).
|
|
3790
|
+
const firstCtxKey = refFirstCtxKeys[idx];
|
|
3791
|
+
if (firstCtxKey !== undefined && ctx[firstCtxKey] !== undefined) {
|
|
3792
|
+
stack[++stackTop] = undefined;
|
|
3793
|
+
break;
|
|
3794
|
+
}
|
|
3795
|
+
|
|
3796
|
+
// Key is genuinely unknown — wrap as Residual so detection does not depend
|
|
3797
|
+
// on the reference character (respects custom referencePredicate/referenceSerialization).
|
|
3798
|
+
stack[++stackTop] = makeResidual(refKeys[idx]);
|
|
3799
|
+
break;
|
|
3800
|
+
}
|
|
3801
|
+
case OP_MAKE_COLLECTION:
|
|
3802
|
+
{
|
|
3803
|
+
const n = numAt(bytecode[i++]);
|
|
3804
|
+
// Check for unknown items first
|
|
3805
|
+
let hasUnknown = false;
|
|
3806
|
+
for (let j = 0; j < n; j++) {
|
|
3807
|
+
if (needsReconstruct(stack[stackTop - j])) {
|
|
3808
|
+
hasUnknown = true;
|
|
3809
|
+
break;
|
|
3810
|
+
}
|
|
3811
|
+
}
|
|
3812
|
+
if (hasUnknown) {
|
|
3813
|
+
// Keep serialized form (ref keys or sub-expressions) for residual reconstruction
|
|
3814
|
+
const items = new Array(n);
|
|
3815
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
3816
|
+
items[j] = slotSrc(stack[stackTop--]);
|
|
3817
|
+
}
|
|
3818
|
+
stack[++stackTop] = makeResidual(items);
|
|
3819
|
+
} else {
|
|
3820
|
+
// All concrete — use actual values
|
|
3821
|
+
const items = new Array(n);
|
|
3822
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
3823
|
+
items[j] = slotVal(stack[stackTop--]);
|
|
3824
|
+
}
|
|
3825
|
+
stack[++stackTop] = items;
|
|
3826
|
+
}
|
|
3827
|
+
break;
|
|
3828
|
+
}
|
|
3829
|
+
case OP_PUSH_CONST:
|
|
3830
|
+
stack[++stackTop] = compiled.consts[numAt(bytecode[i++])];
|
|
3831
|
+
break;
|
|
3832
|
+
case OP_OVERLAP_CONST:
|
|
3833
|
+
{
|
|
3834
|
+
const opcodePos = i - 1; // position of OP_OVERLAP_CONST in bytecode
|
|
3835
|
+
const constArr = compiled.consts[numAt(bytecode[i++])];
|
|
3836
|
+
const constOnLeft = directionMap.get(opcodePos) === 0;
|
|
3837
|
+
const dynamic = stack[stackTop--];
|
|
3838
|
+
if (needsReconstruct(dynamic)) {
|
|
3839
|
+
// Dynamic side is unknown — reconstruct as OVERLAP expression preserving original order
|
|
3840
|
+
const constInput = constArr;
|
|
3841
|
+
const dynamicInput = slotSrc(dynamic);
|
|
3842
|
+
stack[++stackTop] = makeResidual([opNames[OP_OVERLAP], constOnLeft ? constInput : dynamicInput, constOnLeft ? dynamicInput : constInput]);
|
|
3843
|
+
break;
|
|
3844
|
+
}
|
|
3845
|
+
const dynamicVal = slotVal(dynamic);
|
|
3846
|
+
if (dynamicVal === null || dynamicVal === undefined || !Array.isArray(dynamicVal)) {
|
|
3847
|
+
stack[++stackTop] = false;
|
|
3848
|
+
break;
|
|
3849
|
+
}
|
|
3850
|
+
if (constArr.length === 0 && dynamicVal.length === 0) {
|
|
3851
|
+
stack[++stackTop] = true;
|
|
3852
|
+
break;
|
|
3853
|
+
}
|
|
3854
|
+
let found = false;
|
|
3855
|
+
if (constArr.length === 1) {
|
|
3856
|
+
const constVal = constArr[0];
|
|
3857
|
+
for (let j = 0; j < dynamicVal.length; j++) {
|
|
3858
|
+
if (dynamicVal[j] === constVal) {
|
|
3859
|
+
found = true;
|
|
3860
|
+
break;
|
|
3861
|
+
}
|
|
3862
|
+
}
|
|
3863
|
+
} else {
|
|
3864
|
+
let s = overlapSetCache.get(constArr);
|
|
3865
|
+
if (s === undefined) {
|
|
3866
|
+
s = new Set(constArr);
|
|
3867
|
+
overlapSetCache.set(constArr, s);
|
|
3868
|
+
}
|
|
3869
|
+
for (let j = 0; j < dynamicVal.length; j++) {
|
|
3870
|
+
if (s.has(dynamicVal[j])) {
|
|
3871
|
+
found = true;
|
|
3872
|
+
break;
|
|
3873
|
+
}
|
|
3874
|
+
}
|
|
3875
|
+
}
|
|
3876
|
+
stack[++stackTop] = found;
|
|
3877
|
+
break;
|
|
3878
|
+
}
|
|
3879
|
+
case OP_OVERLAP_SCAN_REFS_CONST:
|
|
3880
|
+
{
|
|
3881
|
+
// bytecode layout: N, ref0..refN-1, constIdx
|
|
3882
|
+
const opcodePos = i - 1; // position of OP_OVERLAP_SCAN_REFS_CONST in bytecode
|
|
3883
|
+
const n = numAt(bytecode[i++]);
|
|
3884
|
+
const refStart = i;
|
|
3885
|
+
i += n; // advance past all ref indices
|
|
3886
|
+
const constIdx = numAt(bytecode[i++]);
|
|
3887
|
+
const constArr = compiled.consts[constIdx];
|
|
3888
|
+
const constOnLeft = directionMap.get(opcodePos) === 0;
|
|
3889
|
+
|
|
3890
|
+
// First pass: check whether any ref is genuinely unknown.
|
|
3891
|
+
// Uses the per-call ref cache to avoid redundant resolveCompactRef calls.
|
|
3892
|
+
let hasUnknown = false;
|
|
3893
|
+
for (let j = 0; j < n; j++) {
|
|
3894
|
+
const refIdx = numAt(bytecode[refStart + j]);
|
|
3895
|
+
const rawKey = refRawKeys[refIdx];
|
|
3896
|
+
let val;
|
|
3897
|
+
if (resolvedRefDirty[refIdx]) {
|
|
3898
|
+
val = resolvedRefCache[refIdx];
|
|
3899
|
+
} else {
|
|
3900
|
+
val = resolveCompactRef(refs[refIdx], ctx);
|
|
3901
|
+
resolvedRefCache[refIdx] = val;
|
|
3902
|
+
resolvedRefDirty[refIdx] = true;
|
|
3903
|
+
resolvedRefUsed[resolvedRefUsedCount++] = refIdx;
|
|
3904
|
+
}
|
|
3905
|
+
if (val === undefined) {
|
|
3906
|
+
if (strictSet?.has(rawKey)) {
|
|
3907
|
+
continue;
|
|
3908
|
+
}
|
|
3909
|
+
if (optionalSet && !optionalSet.has(rawKey)) {
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
// First-key heuristic: if parent object is in context, treat undefined sub-field as concrete
|
|
3913
|
+
const firstCtxKey = refFirstCtxKeys[refIdx];
|
|
3914
|
+
if (firstCtxKey !== undefined && ctx[firstCtxKey] !== undefined) {
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
hasUnknown = true;
|
|
3918
|
+
break;
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
if (hasUnknown) {
|
|
3922
|
+
// Use pre-built residual array from compile time — no allocation here
|
|
3923
|
+
const refInputs = requireMapEntry(overlapRefsResiduals, opcodePos);
|
|
3924
|
+
const constInput = constArr;
|
|
3925
|
+
const dynamicInput = refInputs;
|
|
3926
|
+
stack[++stackTop] = makeResidual([opNames[OP_OVERLAP], constOnLeft ? constInput : dynamicInput, constOnLeft ? dynamicInput : constInput]);
|
|
3927
|
+
break;
|
|
3928
|
+
}
|
|
3929
|
+
let found = false;
|
|
3930
|
+
if (constArr.length === 1) {
|
|
3931
|
+
// Fast path: single-element const — direct equality, no Set needed
|
|
3932
|
+
const constVal = constArr[0];
|
|
3933
|
+
for (let j = 0; j < n; j++) {
|
|
3934
|
+
const val = resolvedRefCache[numAt(bytecode[refStart + j])];
|
|
3935
|
+
if (val !== undefined && val === constVal) {
|
|
3936
|
+
found = true;
|
|
3937
|
+
break;
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
} else {
|
|
3941
|
+
let s = overlapSetCache.get(constArr);
|
|
3942
|
+
if (s === undefined) {
|
|
3943
|
+
s = new Set(constArr);
|
|
3944
|
+
overlapSetCache.set(constArr, s);
|
|
3945
|
+
}
|
|
3946
|
+
for (let j = 0; j < n; j++) {
|
|
3947
|
+
const val = resolvedRefCache[numAt(bytecode[refStart + j])];
|
|
3948
|
+
if (val !== undefined && s.has(val)) {
|
|
3949
|
+
found = true;
|
|
3950
|
+
break;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
stack[++stackTop] = found;
|
|
3955
|
+
break;
|
|
3956
|
+
}
|
|
3957
|
+
case OP_IN_SCAN_REFS_CONST:
|
|
3958
|
+
case OP_NOT_IN_SCAN_REFS_CONST:
|
|
3959
|
+
{
|
|
3960
|
+
// bytecode layout: N, ref0..refN-1, constIdx
|
|
3961
|
+
// const at constIdx is a 1-element array [scalar]
|
|
3962
|
+
const opcodePos = i - 1;
|
|
3963
|
+
const n = numAt(bytecode[i++]);
|
|
3964
|
+
const refStart = i;
|
|
3965
|
+
i += n;
|
|
3966
|
+
const constIdx = numAt(bytecode[i++]);
|
|
3967
|
+
const constArr = compiled.consts[constIdx];
|
|
3968
|
+
// dir=0: refs on left, scalar on right → scalar (constInput) is on right
|
|
3969
|
+
// dir=1: scalar on left, refs on right → scalar (constInput) is on left
|
|
3970
|
+
const constOnLeft = directionMap.get(opcodePos) === 1;
|
|
3971
|
+
let hasUnknown = false;
|
|
3972
|
+
for (let j = 0; j < n; j++) {
|
|
3973
|
+
const refIdx = numAt(bytecode[refStart + j]);
|
|
3974
|
+
const rawKey = refRawKeys[refIdx];
|
|
3975
|
+
let val;
|
|
3976
|
+
if (resolvedRefDirty[refIdx]) {
|
|
3977
|
+
val = resolvedRefCache[refIdx];
|
|
3978
|
+
} else {
|
|
3979
|
+
val = resolveCompactRef(refs[refIdx], ctx);
|
|
3980
|
+
resolvedRefCache[refIdx] = val;
|
|
3981
|
+
resolvedRefDirty[refIdx] = true;
|
|
3982
|
+
resolvedRefUsed[resolvedRefUsedCount++] = refIdx;
|
|
3983
|
+
}
|
|
3984
|
+
if (val === undefined) {
|
|
3985
|
+
if (strictSet?.has(rawKey)) {
|
|
3986
|
+
continue;
|
|
3987
|
+
}
|
|
3988
|
+
if (optionalSet && !optionalSet.has(rawKey)) {
|
|
3989
|
+
continue;
|
|
3990
|
+
}
|
|
3991
|
+
// First-key heuristic: if parent object is in context, treat undefined sub-field as concrete
|
|
3992
|
+
const firstCtxKey = refFirstCtxKeys[refIdx];
|
|
3993
|
+
if (firstCtxKey !== undefined && ctx[firstCtxKey] !== undefined) {
|
|
3994
|
+
continue;
|
|
3995
|
+
}
|
|
3996
|
+
hasUnknown = true;
|
|
3997
|
+
break;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
if (hasUnknown) {
|
|
4001
|
+
const refInputs = requireMapEntry(overlapRefsResiduals, opcodePos);
|
|
4002
|
+
// constArr is a 1-element array [scalar]; use the scalar as the const operand
|
|
4003
|
+
const constInput = constArr[0];
|
|
4004
|
+
const dynamicInput = refInputs;
|
|
4005
|
+
stack[++stackTop] = makeResidual([opNames[op], constOnLeft ? constInput : dynamicInput, constOnLeft ? dynamicInput : constInput]);
|
|
4006
|
+
break;
|
|
4007
|
+
}
|
|
4008
|
+
const target = constArr[0];
|
|
4009
|
+
let found = false;
|
|
4010
|
+
if (target !== null && target !== undefined) {
|
|
4011
|
+
for (let j = 0; j < n; j++) {
|
|
4012
|
+
const val = resolvedRefCache[numAt(bytecode[refStart + j])];
|
|
4013
|
+
if (val === target) {
|
|
4014
|
+
found = true;
|
|
4015
|
+
break;
|
|
4016
|
+
}
|
|
4017
|
+
}
|
|
4018
|
+
}
|
|
4019
|
+
stack[++stackTop] = op === OP_IN_SCAN_REFS_CONST ? found : !found;
|
|
4020
|
+
break;
|
|
4021
|
+
}
|
|
4022
|
+
case OP_STORE_LOCAL:
|
|
4023
|
+
// Peek top (don't pop) and store into locals slot
|
|
4024
|
+
locals[numAt(bytecode[i++])] = stack[stackTop];
|
|
4025
|
+
break;
|
|
4026
|
+
case OP_LOAD_LOCAL:
|
|
4027
|
+
stack[++stackTop] = locals[numAt(bytecode[i++])];
|
|
4028
|
+
break;
|
|
4029
|
+
|
|
4030
|
+
// ---------------------------------------------------------------------
|
|
4031
|
+
// Equality
|
|
4032
|
+
// ---------------------------------------------------------------------
|
|
4033
|
+
case OP_EQ:
|
|
4034
|
+
case OP_NE:
|
|
4035
|
+
{
|
|
4036
|
+
const right = stack[stackTop--];
|
|
4037
|
+
const left = stack[stackTop--];
|
|
4038
|
+
if (needsReconstruct(left) || needsReconstruct(right)) {
|
|
4039
|
+
stack[++stackTop] = makeResidual([opNames[op], slotSrc(left), slotSrc(right)]);
|
|
4040
|
+
} else {
|
|
4041
|
+
const lv = slotVal(left);
|
|
4042
|
+
const rv = slotVal(right);
|
|
4043
|
+
stack[++stackTop] = op === OP_EQ ? lv === rv : lv !== rv;
|
|
4044
|
+
}
|
|
4045
|
+
break;
|
|
4046
|
+
}
|
|
4047
|
+
|
|
4048
|
+
// ---------------------------------------------------------------------
|
|
4049
|
+
// Relational
|
|
4050
|
+
// ---------------------------------------------------------------------
|
|
4051
|
+
case OP_GT:
|
|
4052
|
+
case OP_GE:
|
|
4053
|
+
case OP_LT:
|
|
4054
|
+
case OP_LE:
|
|
4055
|
+
{
|
|
4056
|
+
const right = stack[stackTop--];
|
|
4057
|
+
const left = stack[stackTop--];
|
|
4058
|
+
// Check for DivByZeroMarker on either side
|
|
4059
|
+
const leftIsDivZero = isDivByZeroMarker(left);
|
|
4060
|
+
const rightIsDivZero = isDivByZeroMarker(right);
|
|
4061
|
+
if (leftIsDivZero) {
|
|
4062
|
+
// Left is a division-by-zero result.
|
|
4063
|
+
// Match OOP isInfinite guard: preserve expression only if right
|
|
4064
|
+
// is a residual. If right is concrete, evaluate directly.
|
|
4065
|
+
if (needsReconstruct(right) && !rightIsDivZero) {
|
|
4066
|
+
stack[++stackTop] = makeResidual([opNames[op], ['/', left.left, left.right], slotSrc(right)]);
|
|
4067
|
+
} else {
|
|
4068
|
+
// Both concrete — evaluate directly (OOP: 10000 > Infinity = false)
|
|
4069
|
+
stack[++stackTop] = relationalCompare(left._val, slotVal(right), op);
|
|
4070
|
+
}
|
|
4071
|
+
} else if (rightIsDivZero) {
|
|
4072
|
+
// Right is a division-by-zero result.
|
|
4073
|
+
// If left is a residual, preserve expression. Otherwise evaluate directly.
|
|
4074
|
+
if (needsReconstruct(left)) {
|
|
4075
|
+
stack[++stackTop] = makeResidual([opNames[op], slotSrc(left), ['/', right.left, right.right]]);
|
|
4076
|
+
} else {
|
|
4077
|
+
// Both concrete — evaluate directly (OOP: 10000 > Infinity = false)
|
|
4078
|
+
stack[++stackTop] = relationalCompare(slotVal(left), right._val, op);
|
|
4079
|
+
}
|
|
4080
|
+
} else if (needsReconstruct(left) || needsReconstruct(right)) {
|
|
4081
|
+
stack[++stackTop] = makeResidual([opNames[op], slotSrc(left), slotSrc(right)]);
|
|
4082
|
+
} else {
|
|
4083
|
+
stack[++stackTop] = relationalCompare(slotVal(left), slotVal(right), op);
|
|
4084
|
+
}
|
|
4085
|
+
break;
|
|
4086
|
+
}
|
|
4087
|
+
|
|
4088
|
+
// ---------------------------------------------------------------------
|
|
4089
|
+
// Containment — dynamic fallback (both sides are refs)
|
|
4090
|
+
// ---------------------------------------------------------------------
|
|
4091
|
+
case OP_IN:
|
|
4092
|
+
case OP_NOT_IN:
|
|
4093
|
+
{
|
|
4094
|
+
const right = stack[stackTop--];
|
|
4095
|
+
const left = stack[stackTop--];
|
|
4096
|
+
if (needsReconstruct(left) || needsReconstruct(right)) {
|
|
4097
|
+
stack[++stackTop] = makeResidual([opNames[op], slotSrc(left), slotSrc(right)]);
|
|
4098
|
+
} else {
|
|
4099
|
+
const l = slotVal(left);
|
|
4100
|
+
const r = slotVal(right);
|
|
4101
|
+
if (l === null || l === undefined || r === null || r === undefined) {
|
|
4102
|
+
stack[++stackTop] = op === OP_IN ? false : true;
|
|
4103
|
+
} else if (Array.isArray(l)) {
|
|
4104
|
+
if (Array.isArray(r)) {
|
|
4105
|
+
throw new Error(op === OP_IN ? 'IN: both operands are arrays' : 'NOT IN: both operands are arrays');
|
|
4106
|
+
}
|
|
4107
|
+
const found = l.indexOf(r) > -1;
|
|
4108
|
+
stack[++stackTop] = op === OP_IN ? found : !found;
|
|
4109
|
+
} else if (Array.isArray(r)) {
|
|
4110
|
+
const found = r.indexOf(l) > -1;
|
|
4111
|
+
stack[++stackTop] = op === OP_IN ? found : !found;
|
|
4112
|
+
} else {
|
|
4113
|
+
throw new Error(op === OP_IN ? 'IN: neither operand is an array' : 'NOT IN: neither operand is an array');
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
break;
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
// ---------------------------------------------------------------------
|
|
4120
|
+
// Inline collection scan
|
|
4121
|
+
// ---------------------------------------------------------------------
|
|
4122
|
+
case OP_IN_COLLECTION:
|
|
4123
|
+
case OP_NOT_IN_COLLECTION:
|
|
4124
|
+
{
|
|
4125
|
+
const opcodePos = i - 1; // position of the opcode in bytecode
|
|
4126
|
+
const n = numAt(bytecode[i++]);
|
|
4127
|
+
const collectionOnLeft = directionMap.get(opcodePos) === 0;
|
|
4128
|
+
const scalarSlot = stack[stackTop--];
|
|
4129
|
+
const scalarIsUnknown = needsReconstruct(scalarSlot);
|
|
4130
|
+
// Check if any collection item needs reconstruction
|
|
4131
|
+
let hasUnknownItem = false;
|
|
4132
|
+
for (let j = 0; j < n; j++) {
|
|
4133
|
+
if (needsReconstruct(stack[stackTop - j])) {
|
|
4134
|
+
hasUnknownItem = true;
|
|
4135
|
+
break;
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
if (scalarIsUnknown || hasUnknownItem) {
|
|
4139
|
+
const items = new Array(n);
|
|
4140
|
+
for (let j = 0; j < n; j++) {
|
|
4141
|
+
items[j] = slotSrc(stack[stackTop - (n - 1 - j)]);
|
|
4142
|
+
}
|
|
4143
|
+
stackTop -= n;
|
|
4144
|
+
// Reconstruct preserving original operand order
|
|
4145
|
+
const itemsInput = items;
|
|
4146
|
+
const reconstructed = collectionOnLeft ? [opNames[op], itemsInput, slotSrc(scalarSlot)] : [opNames[op], slotSrc(scalarSlot), itemsInput];
|
|
4147
|
+
stack[++stackTop] = makeResidual(reconstructed);
|
|
4148
|
+
break;
|
|
4149
|
+
}
|
|
4150
|
+
// All concrete — inline scan using slotVal to unwrap Resolved
|
|
4151
|
+
const scalar = slotVal(scalarSlot);
|
|
4152
|
+
let found = false;
|
|
4153
|
+
if (scalar !== null && scalar !== undefined) {
|
|
4154
|
+
for (let j = 0; j < n; j++) {
|
|
4155
|
+
if (slotVal(stack[stackTop - j]) === scalar) {
|
|
4156
|
+
found = true;
|
|
4157
|
+
break;
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
stackTop -= n;
|
|
4162
|
+
stack[++stackTop] = op === OP_IN_COLLECTION ? found : !found;
|
|
4163
|
+
break;
|
|
4164
|
+
}
|
|
4165
|
+
case OP_IN_CONST:
|
|
4166
|
+
case OP_NOT_IN_CONST:
|
|
4167
|
+
{
|
|
4168
|
+
const opcodePos = i - 1; // position of the opcode in bytecode
|
|
4169
|
+
const constArr = compiled.consts[numAt(bytecode[i++])];
|
|
4170
|
+
const collectionOnLeft = directionMap.get(opcodePos) === 0;
|
|
4171
|
+
const scalarSlot = stack[stackTop--];
|
|
4172
|
+
if (needsReconstruct(scalarSlot)) {
|
|
4173
|
+
const scalarInput = slotSrc(scalarSlot);
|
|
4174
|
+
const constInput = constArr;
|
|
4175
|
+
stack[++stackTop] = makeResidual([opNames[op], collectionOnLeft ? constInput : scalarInput, collectionOnLeft ? scalarInput : constInput]);
|
|
4176
|
+
break;
|
|
4177
|
+
}
|
|
4178
|
+
const scalar = slotVal(scalarSlot);
|
|
4179
|
+
if (scalar === null || scalar === undefined) {
|
|
4180
|
+
stack[++stackTop] = op === OP_IN_CONST ? false : true;
|
|
4181
|
+
break;
|
|
4182
|
+
}
|
|
4183
|
+
let s = overlapSetCache.get(constArr);
|
|
4184
|
+
if (s === undefined) {
|
|
4185
|
+
s = new Set(constArr);
|
|
4186
|
+
overlapSetCache.set(constArr, s);
|
|
4187
|
+
}
|
|
4188
|
+
const found = s.has(scalar);
|
|
4189
|
+
stack[++stackTop] = op === OP_IN_CONST ? found : !found;
|
|
4190
|
+
break;
|
|
4191
|
+
}
|
|
4192
|
+
case OP_OR_AND_IN_CONST_2:
|
|
4193
|
+
{
|
|
4194
|
+
// bytecode layout: ref1Idx, ref2Idx, M, aVal0, setBIdx0, aVal1, setBIdx1, ..., aValM-1, setBIdxM-1
|
|
4195
|
+
// aVal_j is a literal value; setBIdx_j is a constIdx for the merged setB.
|
|
4196
|
+
const ref1Idx = numAt(bytecode[i++]);
|
|
4197
|
+
const ref2Idx = numAt(bytecode[i++]);
|
|
4198
|
+
const n = numAt(bytecode[i++]);
|
|
4199
|
+
const pairsStart = i;
|
|
4200
|
+
i += n * 2;
|
|
4201
|
+
const rawKey1 = refRawKeys[ref1Idx];
|
|
4202
|
+
const rawKey2 = refRawKeys[ref2Idx];
|
|
4203
|
+
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
4204
|
+
const v2 = resolveCompactRef(refs[ref2Idx], ctx);
|
|
4205
|
+
const unknown1 = v1 === undefined && !strictSet?.has(rawKey1) && (!optionalSet || optionalSet.has(rawKey1));
|
|
4206
|
+
const unknown2 = v2 === undefined && !strictSet?.has(rawKey2) && (!optionalSet || optionalSet.has(rawKey2));
|
|
4207
|
+
if (unknown1 || unknown2) {
|
|
4208
|
+
// Reconstruct the original complex expression tree
|
|
4209
|
+
const branches = [opNames[OP_OR]];
|
|
4210
|
+
const andOp = opNames[OP_AND];
|
|
4211
|
+
const eqOp = opNames[OP_EQ];
|
|
4212
|
+
const inOp = opNames[OP_IN];
|
|
4213
|
+
const r1 = refKeys[ref1Idx];
|
|
4214
|
+
const r2 = refKeys[ref2Idx];
|
|
4215
|
+
for (let j = 0; j < n; j++) {
|
|
4216
|
+
const aVal = literalAt(bytecode[pairsStart + j * 2]);
|
|
4217
|
+
const setB = compiled.consts[numAt(bytecode[pairsStart + j * 2 + 1])];
|
|
4218
|
+
const setInput = setB;
|
|
4219
|
+
branches.push([andOp, [eqOp, r1, aVal], [inOp, r2, setInput]]);
|
|
4220
|
+
}
|
|
4221
|
+
stack[++stackTop] = makeResidual(branches);
|
|
4222
|
+
break;
|
|
4223
|
+
}
|
|
4224
|
+
|
|
4225
|
+
// Both refs known — evaluate normally
|
|
4226
|
+
let found = false;
|
|
4227
|
+
if (v1 !== null && v1 !== undefined && v2 !== null && v2 !== undefined) {
|
|
4228
|
+
for (let j = 0; j < n; j++) {
|
|
4229
|
+
if (bytecode[pairsStart + j * 2] === v1) {
|
|
4230
|
+
const setB = compiled.consts[numAt(bytecode[pairsStart + j * 2 + 1])];
|
|
4231
|
+
let s = overlapSetCache.get(setB);
|
|
4232
|
+
if (s === undefined) {
|
|
4233
|
+
s = new Set(setB);
|
|
4234
|
+
overlapSetCache.set(setB, s);
|
|
4235
|
+
}
|
|
4236
|
+
found = s.has(v2);
|
|
4237
|
+
break;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
}
|
|
4241
|
+
stack[++stackTop] = found;
|
|
4242
|
+
break;
|
|
4243
|
+
}
|
|
4244
|
+
|
|
4245
|
+
// ---------------------------------------------------------------------
|
|
4246
|
+
// String
|
|
4247
|
+
// ---------------------------------------------------------------------
|
|
4248
|
+
case OP_PREFIX:
|
|
4249
|
+
case OP_SUFFIX:
|
|
4250
|
+
{
|
|
4251
|
+
const right = stack[stackTop--];
|
|
4252
|
+
const left = stack[stackTop--];
|
|
4253
|
+
if (needsReconstruct(left) || needsReconstruct(right)) {
|
|
4254
|
+
stack[++stackTop] = makeResidual([opNames[op], slotSrc(left), slotSrc(right)]);
|
|
4255
|
+
} else {
|
|
4256
|
+
const lv = slotVal(left);
|
|
4257
|
+
const rv = slotVal(right);
|
|
4258
|
+
stack[++stackTop] = op === OP_PREFIX ? isString(lv) && isString(rv) ? rv.startsWith(lv) : false : isString(lv) && isString(rv) ? lv.endsWith(rv) : false;
|
|
4259
|
+
}
|
|
4260
|
+
break;
|
|
4261
|
+
}
|
|
4262
|
+
|
|
4263
|
+
// ---------------------------------------------------------------------
|
|
4264
|
+
// Array
|
|
4265
|
+
// ---------------------------------------------------------------------
|
|
4266
|
+
case OP_OVERLAP:
|
|
4267
|
+
{
|
|
4268
|
+
const right = stack[stackTop--];
|
|
4269
|
+
const left = stack[stackTop--];
|
|
4270
|
+
if (needsReconstruct(left) || needsReconstruct(right)) {
|
|
4271
|
+
stack[++stackTop] = makeResidual([opNames[op], slotSrc(left), slotSrc(right)]);
|
|
4272
|
+
} else {
|
|
4273
|
+
const l = slotVal(left);
|
|
4274
|
+
const r = slotVal(right);
|
|
4275
|
+
if (l === null || l === undefined || r === null || r === undefined) {
|
|
4276
|
+
stack[++stackTop] = false;
|
|
4277
|
+
} else if (!Array.isArray(l) || !Array.isArray(r)) {
|
|
4278
|
+
throw new Error('OVERLAP: both operands must be arrays');
|
|
4279
|
+
} else if (l.length === 0 && r.length === 0) {
|
|
4280
|
+
stack[++stackTop] = true;
|
|
4281
|
+
} else {
|
|
4282
|
+
stack[++stackTop] = l.some(el => r.includes(el));
|
|
4283
|
+
}
|
|
4284
|
+
}
|
|
4285
|
+
break;
|
|
4286
|
+
}
|
|
4287
|
+
|
|
4288
|
+
// ---------------------------------------------------------------------
|
|
4289
|
+
// Presence
|
|
4290
|
+
// ---------------------------------------------------------------------
|
|
4291
|
+
case OP_PRESENT:
|
|
4292
|
+
{
|
|
4293
|
+
const val = stack[stackTop];
|
|
4294
|
+
if (needsReconstruct(val)) {
|
|
4295
|
+
stack[stackTop] = makeResidual([opNames[op], slotSrc(val)]);
|
|
4296
|
+
} else {
|
|
4297
|
+
const v = slotVal(val);
|
|
4298
|
+
stack[stackTop] = v !== undefined && v !== null;
|
|
4299
|
+
}
|
|
4300
|
+
break;
|
|
4301
|
+
}
|
|
4302
|
+
case OP_UNDEFINED:
|
|
4303
|
+
{
|
|
4304
|
+
const val = stack[stackTop];
|
|
4305
|
+
if (needsReconstruct(val)) {
|
|
4306
|
+
stack[stackTop] = makeResidual([opNames[op], slotSrc(val)]);
|
|
4307
|
+
} else {
|
|
4308
|
+
const v = slotVal(val);
|
|
4309
|
+
stack[stackTop] = v === undefined;
|
|
4310
|
+
}
|
|
4311
|
+
break;
|
|
4312
|
+
}
|
|
4313
|
+
|
|
4314
|
+
// ---------------------------------------------------------------------
|
|
4315
|
+
// Arithmetic
|
|
4316
|
+
// ---------------------------------------------------------------------
|
|
4317
|
+
case OP_SUM:
|
|
4318
|
+
case OP_SUBTRACT:
|
|
4319
|
+
case OP_MULTIPLY:
|
|
4320
|
+
case OP_DIVIDE:
|
|
4321
|
+
{
|
|
4322
|
+
const n = numAt(bytecode[i++]);
|
|
4323
|
+
// Check for any unknown operand
|
|
4324
|
+
let hasUnknown = false;
|
|
4325
|
+
for (let j = 0; j < n; j++) {
|
|
4326
|
+
if (needsReconstruct(stack[stackTop - j])) {
|
|
4327
|
+
hasUnknown = true;
|
|
4328
|
+
break;
|
|
4329
|
+
}
|
|
4330
|
+
}
|
|
4331
|
+
if (hasUnknown) {
|
|
4332
|
+
const items = new Array(n);
|
|
4333
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
4334
|
+
items[j] = slotSrc(stack[stackTop--]);
|
|
4335
|
+
}
|
|
4336
|
+
stack[++stackTop] = makeResidual([opNames[op], ...items]);
|
|
4337
|
+
break;
|
|
4338
|
+
}
|
|
4339
|
+
// Fast path: 2 operands, all concrete
|
|
4340
|
+
if (n === 2) {
|
|
4341
|
+
const bVal = slotVal(stack[stackTop--]);
|
|
4342
|
+
const aVal = slotVal(stack[stackTop--]);
|
|
4343
|
+
if (aVal === null || aVal === undefined || bVal === null || bVal === undefined) {
|
|
4344
|
+
stack[++stackTop] = false;
|
|
4345
|
+
break;
|
|
4346
|
+
}
|
|
4347
|
+
const isDateArithmetic = !isNaN(toDateNumber(aVal));
|
|
4348
|
+
if (!isNumber(aVal) || !isNumber(bVal)) {
|
|
4349
|
+
if (op === OP_SUM || op === OP_SUBTRACT) {
|
|
4350
|
+
if (isDateArithmetic) {
|
|
4351
|
+
const duration = toDateDuration(bVal);
|
|
4352
|
+
if (!duration) {
|
|
4353
|
+
throw new Error(`arithmetic operand is not a date duration: ${bVal}`);
|
|
4354
|
+
}
|
|
4355
|
+
} else {
|
|
4356
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4357
|
+
}
|
|
4358
|
+
} else {
|
|
4359
|
+
throw new Error(`arithmetic operand is not a number: ${!isNumber(aVal) ? aVal : bVal}`);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
// After the throw above, aVal and bVal are narrowed to number
|
|
4363
|
+
if (op === OP_SUM) {
|
|
4364
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? addDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'sum'));
|
|
4365
|
+
} else if (op === OP_SUBTRACT) {
|
|
4366
|
+
stack[++stackTop] = !isDateArithmetic && isNumber(aVal) && isNumber(bVal) ? subtractDecimals(aVal, bVal) : formatDateNumber(mutateDateWithDuration(toDateNumber(aVal), toDateDuration(bVal), 'subtract'));
|
|
4367
|
+
} else if (isNumber(aVal) && isNumber(bVal)) {
|
|
4368
|
+
if (op === OP_MULTIPLY) {
|
|
4369
|
+
stack[++stackTop] = multiplyDecimals(aVal, bVal);
|
|
4370
|
+
} else {
|
|
4371
|
+
// Division — match OOP isInfinite guard: use a marker so
|
|
4372
|
+
// comparisons can decide whether to preserve the expression.
|
|
4373
|
+
const result = divideDecimals(aVal, bVal);
|
|
4374
|
+
if (isUnusableResult(result)) {
|
|
4375
|
+
// Push marker with original operands for reconstruction
|
|
4376
|
+
const marker = {
|
|
4377
|
+
_r: 4,
|
|
4378
|
+
_val: result,
|
|
4379
|
+
left: aVal,
|
|
4380
|
+
right: bVal
|
|
4381
|
+
};
|
|
4382
|
+
stack[++stackTop] = marker;
|
|
4383
|
+
} else {
|
|
4384
|
+
stack[++stackTop] = result;
|
|
4385
|
+
}
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
break;
|
|
4389
|
+
}
|
|
4390
|
+
// N-operand path
|
|
4391
|
+
const values = new Array(n);
|
|
4392
|
+
let hasNull = false;
|
|
4393
|
+
const firstOperand = slotVal(stack[stackTop - n + 1]);
|
|
4394
|
+
const isDateArithmetic = typeof firstOperand !== 'object' && !isNaN(toDateNumber(firstOperand)) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
4395
|
+
for (let j = n - 1; j >= 0; j--) {
|
|
4396
|
+
const v = slotVal(stack[stackTop--]);
|
|
4397
|
+
if (v === null || v === undefined) {
|
|
4398
|
+
hasNull = true;
|
|
4399
|
+
break;
|
|
4400
|
+
}
|
|
4401
|
+
if (!isNumber(v)) {
|
|
4402
|
+
if (isDateArithmetic) {
|
|
4403
|
+
const duration = toDateDuration(v);
|
|
4404
|
+
if (!duration && j > 0) {
|
|
4405
|
+
throw new Error(`arithmetic operand is not a date duration: ${v}`);
|
|
4406
|
+
}
|
|
4407
|
+
} else {
|
|
4408
|
+
throw new Error(`arithmetic operand is not a number: ${v}`);
|
|
4409
|
+
}
|
|
4410
|
+
}
|
|
4411
|
+
// After the throw above, v is narrowed to number
|
|
4412
|
+
if (isNumber(v) || isString(v)) {
|
|
4413
|
+
values[j] = v;
|
|
4414
|
+
}
|
|
4415
|
+
}
|
|
4416
|
+
const reduced = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce(values, op) : false;
|
|
4417
|
+
if (hasNull) {
|
|
4418
|
+
stack[++stackTop] = false;
|
|
4419
|
+
} else if (op === OP_DIVIDE && reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4420
|
+
// N-operand division producing Infinity/NaN — create marker
|
|
4421
|
+
const result = reduced;
|
|
4422
|
+
const marker = {
|
|
4423
|
+
_r: 4,
|
|
4424
|
+
_val: result,
|
|
4425
|
+
left: values[0],
|
|
4426
|
+
right: values[1]
|
|
4427
|
+
};
|
|
4428
|
+
stack[++stackTop] = marker;
|
|
4429
|
+
} else if (reduced !== false && isNumber(reduced) && isUnusableResult(reduced)) {
|
|
4430
|
+
// Other arithmetic producing Infinity/NaN — preserve expression
|
|
4431
|
+
stack[++stackTop] = makeResidual([opNames[op], ...values]);
|
|
4432
|
+
} else {
|
|
4433
|
+
stack[++stackTop] = reduced;
|
|
4434
|
+
}
|
|
4435
|
+
break;
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
// ---------------------------------------------------------------------
|
|
4439
|
+
// Logical
|
|
4440
|
+
// ---------------------------------------------------------------------
|
|
4441
|
+
case OP_NOT:
|
|
4442
|
+
{
|
|
4443
|
+
const val = stack[stackTop];
|
|
4444
|
+
if (needsReconstruct(val)) {
|
|
4445
|
+
const src = slotSrc(val);
|
|
4446
|
+
// If this NOT follows a NOR that already applied its simplification
|
|
4447
|
+
// (multi-residual NOR pushes a NOR expression), pass it through unchanged.
|
|
4448
|
+
if (Array.isArray(src) && src[0] === opNames[OP_NOR]) {
|
|
4449
|
+
// Already a NOR expression — leave it as-is
|
|
4450
|
+
break;
|
|
4451
|
+
}
|
|
4452
|
+
stack[stackTop] = makeResidual([opNames[op], src]);
|
|
4453
|
+
} else {
|
|
4454
|
+
const v = slotVal(val);
|
|
4455
|
+
if (typeof v !== 'boolean') {
|
|
4456
|
+
throw new Error('NOT: operand must be boolean');
|
|
4457
|
+
}
|
|
4458
|
+
stack[stackTop] = !v;
|
|
4459
|
+
}
|
|
4460
|
+
break;
|
|
4461
|
+
}
|
|
4462
|
+
case OP_JUMP_IF_FALSE:
|
|
4463
|
+
{
|
|
4464
|
+
const offset = numAt(bytecode[i++]);
|
|
4465
|
+
const top = stack[stackTop];
|
|
4466
|
+
if (!needsReconstruct(top) && slotVal(top) === false) {
|
|
4467
|
+
i += offset;
|
|
4468
|
+
}
|
|
4469
|
+
// Clear spill buffer when transitioning between short-circuit sequences.
|
|
4470
|
+
// Different jump opcodes (41=JUMP_IF_FALSE for AND vs 42=JUMP_IF_TRUE for OR/NOR)
|
|
4471
|
+
// indicate different short-circuit sequences.
|
|
4472
|
+
if (lastJumpOp !== 41) {
|
|
4473
|
+
spillTop = -1;
|
|
4474
|
+
}
|
|
4475
|
+
lastJumpOp = 41;
|
|
4476
|
+
break;
|
|
4477
|
+
}
|
|
4478
|
+
case OP_JUMP_IF_TRUE:
|
|
4479
|
+
{
|
|
4480
|
+
const offset = numAt(bytecode[i++]);
|
|
4481
|
+
const top = stack[stackTop];
|
|
4482
|
+
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4483
|
+
i += offset;
|
|
4484
|
+
}
|
|
4485
|
+
// Clear spill buffer when transitioning between short-circuit sequences.
|
|
4486
|
+
if (lastJumpOp !== 42) {
|
|
4487
|
+
spillTop = -1;
|
|
4488
|
+
}
|
|
4489
|
+
lastJumpOp = 42;
|
|
4490
|
+
break;
|
|
4491
|
+
}
|
|
4492
|
+
case OP_POP:
|
|
4493
|
+
{
|
|
4494
|
+
const popped = stack[stackTop--];
|
|
4495
|
+
// If this residual is being discarded in a short-circuit sequence,
|
|
4496
|
+
// save it so OP_AND/OR/NOR can include it in the reconstructed expression.
|
|
4497
|
+
if (needsReconstruct(popped)) {
|
|
4498
|
+
spillBuf[++spillTop] = popped;
|
|
4499
|
+
}
|
|
4500
|
+
break;
|
|
4501
|
+
}
|
|
4502
|
+
|
|
4503
|
+
// AND/OR/NOR markers — collect the current stack top plus any residuals
|
|
4504
|
+
// that were spilled by OP_POP during the short-circuit sequence, then
|
|
4505
|
+
// apply the simplification logic.
|
|
4506
|
+
case OP_AND:
|
|
4507
|
+
{
|
|
4508
|
+
i++; // consume the operand count byte (unused — we use the spill buffer)
|
|
4509
|
+
const top = stack[stackTop--];
|
|
4510
|
+
// Fast path: nothing was spilled — all non-top operands were concrete.
|
|
4511
|
+
// The top is either a short-circuit false, the last true, or a lone residual.
|
|
4512
|
+
// Push top directly — slotVal unwrapping happens at the final return point.
|
|
4513
|
+
if (spillTop < 0) {
|
|
4514
|
+
spillTop = -1;
|
|
4515
|
+
if (!needsReconstruct(top)) {
|
|
4516
|
+
stack[++stackTop] = top;
|
|
4517
|
+
} else {
|
|
4518
|
+
stack[++stackTop] = makeResidual(slotSrc(top));
|
|
4519
|
+
}
|
|
4520
|
+
break;
|
|
4521
|
+
}
|
|
4522
|
+
const residuals = [];
|
|
4523
|
+
// Drain spill buffer (residuals from earlier operands that were POP'd)
|
|
4524
|
+
// Check if any spilled operand was false (dominates AND)
|
|
4525
|
+
let andDominated = false;
|
|
4526
|
+
for (let j = 0; j <= spillTop; j++) {
|
|
4527
|
+
const v = spillBuf[j];
|
|
4528
|
+
if (!needsReconstruct(v) && slotVal(v) === false) {
|
|
4529
|
+
andDominated = true;
|
|
4530
|
+
break;
|
|
4531
|
+
}
|
|
4532
|
+
if (needsReconstruct(v)) {
|
|
4533
|
+
residuals.push(slotSrc(v));
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
spillTop = -1; // clear spill buffer
|
|
4537
|
+
// Include the stack top (last operand result)
|
|
4538
|
+
if (!andDominated) {
|
|
4539
|
+
if (!needsReconstruct(top) && slotVal(top) === false) {
|
|
4540
|
+
andDominated = true;
|
|
4541
|
+
} else if (needsReconstruct(top)) {
|
|
4542
|
+
residuals.push(slotSrc(top));
|
|
4543
|
+
}
|
|
4544
|
+
}
|
|
4545
|
+
if (andDominated) {
|
|
4546
|
+
stack[++stackTop] = false;
|
|
4547
|
+
} else if (residuals.length === 0) {
|
|
4548
|
+
stack[++stackTop] = true;
|
|
4549
|
+
} else if (residuals.length === 1) {
|
|
4550
|
+
stack[++stackTop] = makeResidual(residuals[0]);
|
|
4551
|
+
} else {
|
|
4552
|
+
stack[++stackTop] = makeResidual([opNames[op], ...residuals]);
|
|
4553
|
+
}
|
|
4554
|
+
break;
|
|
4555
|
+
}
|
|
4556
|
+
case OP_OR:
|
|
4557
|
+
{
|
|
4558
|
+
i++; // consume the operand count byte
|
|
4559
|
+
const top = stack[stackTop--];
|
|
4560
|
+
// Fast path: nothing was spilled — all non-top operands were concrete.
|
|
4561
|
+
// Push top directly — slotVal unwrapping happens at the final return point.
|
|
4562
|
+
if (spillTop < 0) {
|
|
4563
|
+
spillTop = -1;
|
|
4564
|
+
if (!needsReconstruct(top)) {
|
|
4565
|
+
stack[++stackTop] = top;
|
|
4566
|
+
} else {
|
|
4567
|
+
stack[++stackTop] = makeResidual(slotSrc(top));
|
|
4568
|
+
}
|
|
4569
|
+
break;
|
|
4570
|
+
}
|
|
4571
|
+
const residuals = [];
|
|
4572
|
+
let orDominated = false;
|
|
4573
|
+
for (let j = 0; j <= spillTop; j++) {
|
|
4574
|
+
const v = spillBuf[j];
|
|
4575
|
+
if (!needsReconstruct(v) && slotVal(v) === true) {
|
|
4576
|
+
orDominated = true;
|
|
4577
|
+
break;
|
|
4578
|
+
}
|
|
4579
|
+
if (needsReconstruct(v)) {
|
|
4580
|
+
residuals.push(slotSrc(v));
|
|
4581
|
+
}
|
|
4582
|
+
}
|
|
4583
|
+
spillTop = -1;
|
|
4584
|
+
if (!orDominated) {
|
|
4585
|
+
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4586
|
+
orDominated = true;
|
|
4587
|
+
} else if (needsReconstruct(top)) {
|
|
4588
|
+
residuals.push(slotSrc(top));
|
|
4589
|
+
}
|
|
4590
|
+
}
|
|
4591
|
+
if (orDominated) {
|
|
4592
|
+
stack[++stackTop] = true;
|
|
4593
|
+
} else if (residuals.length === 0) {
|
|
4594
|
+
stack[++stackTop] = false;
|
|
4595
|
+
} else if (residuals.length === 1) {
|
|
4596
|
+
stack[++stackTop] = makeResidual(residuals[0]);
|
|
4597
|
+
} else {
|
|
4598
|
+
stack[++stackTop] = makeResidual([opNames[op], ...residuals]);
|
|
4599
|
+
}
|
|
4600
|
+
break;
|
|
4601
|
+
}
|
|
4602
|
+
case OP_NOR:
|
|
4603
|
+
{
|
|
4604
|
+
// Semantics: NOR = NOT OR. OP_NOT follows in bytecode.
|
|
4605
|
+
// For concrete booleans, we push the OR result so OP_NOT negates it.
|
|
4606
|
+
// For residuals with >1 terms, we apply NOR simplification directly
|
|
4607
|
+
// (returning a NOR expression) and push a "NOR_DONE" residual that
|
|
4608
|
+
// OP_NOT will pass through unchanged.
|
|
4609
|
+
i++; // consume the operand count byte
|
|
4610
|
+
const top = stack[stackTop--];
|
|
4611
|
+
const residuals = [];
|
|
4612
|
+
let dominated = false;
|
|
4613
|
+
for (let j = 0; j <= spillTop; j++) {
|
|
4614
|
+
const v = spillBuf[j];
|
|
4615
|
+
if (!needsReconstruct(v) && slotVal(v) === true) {
|
|
4616
|
+
dominated = true;
|
|
4617
|
+
break;
|
|
4618
|
+
}
|
|
4619
|
+
if (needsReconstruct(v)) {
|
|
4620
|
+
residuals.push(slotSrc(v));
|
|
4621
|
+
}
|
|
4622
|
+
}
|
|
4623
|
+
spillTop = -1;
|
|
4624
|
+
if (!dominated) {
|
|
4625
|
+
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4626
|
+
dominated = true;
|
|
4627
|
+
} else if (needsReconstruct(top)) {
|
|
4628
|
+
residuals.push(slotSrc(top));
|
|
4629
|
+
}
|
|
4630
|
+
}
|
|
4631
|
+
if (dominated) {
|
|
4632
|
+
// OR was true → push true so OP_NOT yields false (= NOR false)
|
|
4633
|
+
stack[++stackTop] = true;
|
|
4634
|
+
} else if (residuals.length === 0) {
|
|
4635
|
+
// All false → push false so OP_NOT yields true (= NOR true)
|
|
4636
|
+
stack[++stackTop] = false;
|
|
4637
|
+
} else if (residuals.length === 1) {
|
|
4638
|
+
// Single residual — OP_NOT will wrap in NOT(residual) ✓
|
|
4639
|
+
stack[++stackTop] = makeResidual(residuals[0]);
|
|
4640
|
+
} else {
|
|
4641
|
+
// Multiple residuals — push NOR expression directly.
|
|
4642
|
+
// OP_NOT must pass it through unchanged (handled in OP_NOT case).
|
|
4643
|
+
stack[++stackTop] = makeResidual([opNames[op], ...residuals]);
|
|
4644
|
+
}
|
|
4645
|
+
break;
|
|
4646
|
+
}
|
|
4647
|
+
case OP_XOR:
|
|
4648
|
+
{
|
|
4649
|
+
// XOR is compiled as chained binary: (((A XOR B) XOR C) XOR D).
|
|
4650
|
+
// We accumulate residuals + trueCount into a XorState to defer finalization
|
|
4651
|
+
// until we know all operands. The XorState is finalized when used by another
|
|
4652
|
+
// operator or at the return point.
|
|
4653
|
+
const b = stack[stackTop--];
|
|
4654
|
+
const a = stack[stackTop--];
|
|
4655
|
+
|
|
4656
|
+
// If neither has unknowns, compute the binary XOR directly (no state needed)
|
|
4657
|
+
// But for "one-hot" XOR semantics, if both are true, we need XorState(trueCount=2)
|
|
4658
|
+
// so subsequent chained XORs also see trueCount > 1.
|
|
4659
|
+
if (!needsReconstruct(a) && !needsReconstruct(b)) {
|
|
4660
|
+
const av = slotVal(a) === true;
|
|
4661
|
+
const bv = slotVal(b) === true;
|
|
4662
|
+
const trueCount = (av ? 1 : 0) + (bv ? 1 : 0);
|
|
4663
|
+
if (trueCount > 1) {
|
|
4664
|
+
// Both true → "one-hot" XOR is false, but track trueCount for chained XORs
|
|
4665
|
+
stack[++stackTop] = makeXorState([], trueCount);
|
|
4666
|
+
} else {
|
|
4667
|
+
stack[++stackTop] = (av || bv) && !(av && bv);
|
|
4668
|
+
}
|
|
4669
|
+
break;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
// Accumulate into a XorState
|
|
4673
|
+
let residuals;
|
|
4674
|
+
let trueCount;
|
|
4675
|
+
if (isXorState(a)) {
|
|
4676
|
+
residuals = a.xorResiduals.slice();
|
|
4677
|
+
trueCount = a.xorTrueCount;
|
|
4678
|
+
} else if (!needsReconstruct(a)) {
|
|
4679
|
+
residuals = [];
|
|
4680
|
+
trueCount = slotVal(a) ? 1 : 0;
|
|
4681
|
+
} else {
|
|
4682
|
+
residuals = [slotSrc(a)];
|
|
4683
|
+
trueCount = 0;
|
|
4684
|
+
}
|
|
4685
|
+
if (isXorState(b)) {
|
|
4686
|
+
for (const r of b.xorResiduals) {
|
|
4687
|
+
residuals.push(r);
|
|
4688
|
+
}
|
|
4689
|
+
trueCount += b.xorTrueCount;
|
|
4690
|
+
} else if (!needsReconstruct(b)) {
|
|
4691
|
+
if (slotVal(b)) {
|
|
4692
|
+
trueCount++;
|
|
4693
|
+
}
|
|
4694
|
+
} else {
|
|
4695
|
+
residuals.push(slotSrc(b));
|
|
4696
|
+
}
|
|
4697
|
+
|
|
4698
|
+
// If no residuals, resolve immediately
|
|
4699
|
+
// "One-hot" XOR: exactly one true → true, otherwise false
|
|
4700
|
+
// This matches the OOP evaluator's behavior.
|
|
4701
|
+
// Use XorState so subsequent chained XORs also see trueCount > 1.
|
|
4702
|
+
if (trueCount > 1) {
|
|
4703
|
+
stack[++stackTop] = makeXorState([], trueCount);
|
|
4704
|
+
break;
|
|
4705
|
+
}
|
|
4706
|
+
if (residuals.length === 0) {
|
|
4707
|
+
stack[++stackTop] = trueCount % 2 === 1;
|
|
4708
|
+
break;
|
|
4709
|
+
}
|
|
4710
|
+
|
|
4711
|
+
// Push accumulated state — finalized by slotSrc() when consumed by another op
|
|
4712
|
+
stack[++stackTop] = makeXorState(residuals, trueCount);
|
|
4713
|
+
break;
|
|
4714
|
+
}
|
|
4715
|
+
default:
|
|
4716
|
+
throw new Error(`unknown opcode: ${op}`);
|
|
4717
|
+
}
|
|
4718
|
+
}
|
|
4719
|
+
|
|
4720
|
+
// Unwrap the top slot to a plain Result | Input
|
|
4721
|
+
const top = stack[stackTop];
|
|
4722
|
+
if (isXorState(top) || isResidual(top)) {
|
|
4723
|
+
return slotSrc(top);
|
|
4724
|
+
}
|
|
4725
|
+
const result = slotVal(top);
|
|
4726
|
+
if (result === undefined) {
|
|
4727
|
+
throw new Error('simplify: unexpected undefined top-level result');
|
|
4728
|
+
}
|
|
4729
|
+
if (!isInput(result)) {
|
|
4730
|
+
throw new Error('simplify: unexpected non-Input result');
|
|
4731
|
+
}
|
|
4732
|
+
return result;
|
|
4733
|
+
}
|
|
4734
|
+
|
|
4735
|
+
class BytecodeEvaluable {
|
|
4736
|
+
constructor(compiled, delegate) {
|
|
4737
|
+
this.compiled = compiled;
|
|
4738
|
+
this.delegate = delegate;
|
|
4739
|
+
_defineProperty(this, "type", EvaluableType.Expression);
|
|
4740
|
+
}
|
|
4741
|
+
evaluate(ctx) {
|
|
4742
|
+
return interpret(this.compiled, ctx);
|
|
4743
|
+
}
|
|
4744
|
+
simplify(ctx, strictKeys, optionalKeys) {
|
|
4745
|
+
return interpretSimplify(this.compiled, ctx, strictKeys, optionalKeys);
|
|
4746
|
+
}
|
|
4747
|
+
serialize(options) {
|
|
4748
|
+
return this.delegate.serialize(options);
|
|
4749
|
+
}
|
|
4750
|
+
toString() {
|
|
4751
|
+
return this.delegate.toString();
|
|
4752
|
+
}
|
|
4753
|
+
}
|
|
4754
|
+
|
|
4755
|
+
/**
|
|
4756
|
+
* Collection operand resolved containing mixture of value and references.
|
|
4757
|
+
*/
|
|
4758
|
+
class Collection extends Operand {
|
|
4759
|
+
/**
|
|
4760
|
+
* Get the items in the collection.
|
|
4761
|
+
* @returns {Array<Value | Reference>}
|
|
4762
|
+
*/
|
|
4763
|
+
getItems() {
|
|
4764
|
+
return this.items;
|
|
4765
|
+
}
|
|
4766
|
+
|
|
4767
|
+
/**
|
|
4768
|
+
* @constructor
|
|
4769
|
+
* @param {Operand[]} items Collection of operands.
|
|
4770
|
+
*/
|
|
4771
|
+
constructor(items) {
|
|
4772
|
+
super();
|
|
4773
|
+
_defineProperty(this, "items", void 0);
|
|
4774
|
+
this.items = items;
|
|
4775
|
+
}
|
|
4776
|
+
|
|
4777
|
+
/**
|
|
4778
|
+
* Evaluate in the given context.
|
|
4779
|
+
* @param {Context} ctx
|
|
4780
|
+
* @return {boolean}
|
|
4781
|
+
*/
|
|
4782
|
+
evaluate(ctx) {
|
|
4783
|
+
return this.items.map(item => item.evaluate(ctx));
|
|
4784
|
+
}
|
|
4785
|
+
|
|
4786
|
+
/**
|
|
4787
|
+
* {@link Evaluable.simplify}
|
|
4788
|
+
*/
|
|
4789
|
+
simplify(...args) {
|
|
4790
|
+
const values = [];
|
|
4791
|
+
for (const item of this.items) {
|
|
4792
|
+
const simplifiedItem = item.simplify(...args);
|
|
4793
|
+
if (isEvaluable(simplifiedItem)) {
|
|
4794
|
+
return this;
|
|
4795
|
+
}
|
|
4796
|
+
values.push(simplifiedItem);
|
|
4797
|
+
}
|
|
4798
|
+
return values;
|
|
4799
|
+
}
|
|
4800
|
+
|
|
4801
|
+
/**
|
|
4802
|
+
* {@link Evaluable.serialize}
|
|
4803
|
+
*/
|
|
4804
|
+
serialize(options) {
|
|
4805
|
+
return this.items.map(item => isEvaluable(item) ? item.serialize(options) : item);
|
|
4806
|
+
}
|
|
4807
|
+
|
|
4808
|
+
/**
|
|
4809
|
+
* Get the strict representation of the operand.
|
|
4810
|
+
* @return {string}
|
|
4811
|
+
*/
|
|
4812
|
+
toString() {
|
|
4813
|
+
return '[' + this.items.map(item => item.toString()).join(', ') + ']';
|
|
4814
|
+
}
|
|
4815
|
+
}
|
|
4816
|
+
|
|
4817
|
+
// Option value whitelist
|
|
4818
|
+
|
|
4819
|
+
/**
|
|
4820
|
+
* Engine evaluator mode.
|
|
4821
|
+
* - `'oop'` (default): evaluates expressions using the classic OOP evaluable tree.
|
|
4822
|
+
* - `'bytecode'`: compiles expressions to bytecode and interprets them.
|
|
4823
|
+
*/
|
|
4824
|
+
|
|
4825
|
+
// Parser options
|
|
4826
|
+
|
|
4827
|
+
/**
|
|
4828
|
+
* Default reference predicate.
|
|
4829
|
+
* The "$" symbol at the begging of the operand is used
|
|
4830
|
+
* to predicate the reference type.
|
|
4831
|
+
* E.g. "$State", "$Country"
|
|
4832
|
+
* @param {string} key
|
|
4833
|
+
* @return {boolean}
|
|
4834
|
+
*/
|
|
4835
|
+
function defaultReferencePredicate(key) {
|
|
4836
|
+
return typeof key === 'string' && key[0] === '$';
|
|
1692
4837
|
}
|
|
1693
4838
|
|
|
1694
4839
|
/**
|
|
@@ -1754,7 +4899,7 @@ class Parser {
|
|
|
1754
4899
|
if (options) {
|
|
1755
4900
|
for (const key of Object.keys(options)) {
|
|
1756
4901
|
if (key in this.opts) {
|
|
1757
|
-
this.opts
|
|
4902
|
+
Reflect.set(this.opts, key, Reflect.get(options, key));
|
|
1758
4903
|
}
|
|
1759
4904
|
}
|
|
1760
4905
|
}
|
|
@@ -1809,6 +4954,9 @@ class Parser {
|
|
|
1809
4954
|
let expression;
|
|
1810
4955
|
let operandParser = this.getOperand;
|
|
1811
4956
|
const operator = raw[0];
|
|
4957
|
+
if (typeof operator !== 'string') {
|
|
4958
|
+
return this.getOperand(raw);
|
|
4959
|
+
}
|
|
1812
4960
|
const operands = raw.slice(1);
|
|
1813
4961
|
|
|
1814
4962
|
/**
|
|
@@ -1961,7 +5109,19 @@ class Engine {
|
|
|
1961
5109
|
*/
|
|
1962
5110
|
constructor(options) {
|
|
1963
5111
|
_defineProperty(this, "parser", void 0);
|
|
5112
|
+
_defineProperty(this, "evaluator", void 0);
|
|
5113
|
+
_defineProperty(this, "bytecodeCache", new WeakMap());
|
|
1964
5114
|
this.parser = new Parser(options);
|
|
5115
|
+
this.evaluator = options?.evaluator ?? 'oop';
|
|
5116
|
+
}
|
|
5117
|
+
getCompiled(exp) {
|
|
5118
|
+
let compiled = this.bytecodeCache.get(exp);
|
|
5119
|
+
if (compiled === undefined) {
|
|
5120
|
+
this.parser.parse(exp); // validates root operator and expression structure
|
|
5121
|
+
compiled = compile(exp, this.parser.options);
|
|
5122
|
+
this.bytecodeCache.set(exp, compiled);
|
|
5123
|
+
}
|
|
5124
|
+
return compiled;
|
|
1965
5125
|
}
|
|
1966
5126
|
|
|
1967
5127
|
/**
|
|
@@ -1971,7 +5131,7 @@ class Engine {
|
|
|
1971
5131
|
* @return {boolean}
|
|
1972
5132
|
*/
|
|
1973
5133
|
evaluate(exp, ctx) {
|
|
1974
|
-
const result = this.parse(exp).evaluate(ctx);
|
|
5134
|
+
const result = this.evaluator === 'oop' ? this.parser.parse(exp).evaluate(ctx) : interpret(this.getCompiled(exp), ctx);
|
|
1975
5135
|
if (isBoolean(result)) {
|
|
1976
5136
|
return result;
|
|
1977
5137
|
}
|
|
@@ -1993,7 +5153,10 @@ class Engine {
|
|
|
1993
5153
|
* @return {Evaluable}
|
|
1994
5154
|
*/
|
|
1995
5155
|
parse(exp) {
|
|
1996
|
-
|
|
5156
|
+
if (this.evaluator === 'oop') {
|
|
5157
|
+
return this.parser.parse(exp);
|
|
5158
|
+
}
|
|
5159
|
+
return new BytecodeEvaluable(this.getCompiled(exp), this.parser.parse(exp));
|
|
1997
5160
|
}
|
|
1998
5161
|
|
|
1999
5162
|
/**
|
|
@@ -2014,7 +5177,10 @@ class Engine {
|
|
|
2014
5177
|
* @returns {Inpunt | boolean}
|
|
2015
5178
|
*/
|
|
2016
5179
|
simplify(exp, context, strictKeys, optionalKeys) {
|
|
2017
|
-
|
|
5180
|
+
if (this.evaluator === 'bytecode') {
|
|
5181
|
+
return interpretSimplify(this.getCompiled(exp), context, strictKeys, optionalKeys);
|
|
5182
|
+
}
|
|
5183
|
+
const result = this.parser.parse(exp).simplify(context, strictKeys, optionalKeys);
|
|
2018
5184
|
if (isEvaluable(result)) {
|
|
2019
5185
|
return result.serialize(this.parser.options);
|
|
2020
5186
|
}
|