@briza/illogical 2.0.2 → 2.1.0

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