@briza/illogical 2.2.0 → 2.2.2

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
@@ -1810,6 +1810,12 @@ const OP_NOR = 55; // next: N — marker for NOR with N operands
1810
1810
  const OP_IN_SCAN_REFS_CONST = 56;
1811
1811
  // next: N, ref0..refN-1, constIdx — resolve each ref inline, check ∉ consts[constIdx], no stack alloc
1812
1812
  const OP_NOT_IN_SCAN_REFS_CONST = 57;
1813
+ // Marks the entry of a short-circuit AND/OR/NOR scope.
1814
+ // Emitted by the compiler at the start of every emitShortCircuit block.
1815
+ // The evaluate interpreter treats it as a no-op.
1816
+ // The simplify interpreter uses it to push a spill-buffer scope marker so that
1817
+ // nested AND/OR/NOR blocks cannot steal residuals accumulated by an outer scope.
1818
+ const OP_ENTER_SCOPE = 58;
1813
1819
 
1814
1820
  /**
1815
1821
  * Reference path descriptors for the bytecode compiler and interpreter.
@@ -2074,6 +2080,36 @@ function resolveCompactRef(ref, ctx) {
2074
2080
  }
2075
2081
  return resolveTokens(ref.tokens ?? [], ref.t, ctx);
2076
2082
  }
2083
+ function getKeyFromCompactRef(ref) {
2084
+ if (typeof ref === 'string') {
2085
+ return ref;
2086
+ }
2087
+ if (Array.isArray(ref)) {
2088
+ return ref.map(ref => ref.includes('.') ? `\`${ref}\`` : ref).join('.');
2089
+ }
2090
+ let tokens = ref.tokens;
2091
+ if (ref.d) {
2092
+ let current = ref.k;
2093
+ let match = dynamicKeyRegex.exec(current);
2094
+ while (match) {
2095
+ current = current.replace(dynamicKeyRegex, '').replace('[]', '');
2096
+ match = dynamicKeyRegex.exec(current);
2097
+ }
2098
+ tokens = parseStaticKey(current);
2099
+ }
2100
+ let key = '';
2101
+ for (const token of tokens ?? []) {
2102
+ if (isNumber(token.value)) {
2103
+ return key;
2104
+ }
2105
+ if (token.value.includes('.')) {
2106
+ key += `${key ? '.' : ''}\`${token.value}\``;
2107
+ } else {
2108
+ key += `${key ? '.' : ''}${token.value}`;
2109
+ }
2110
+ }
2111
+ return key;
2112
+ }
2077
2113
 
2078
2114
  /**
2079
2115
  * Bytecode compiler.
@@ -2389,6 +2425,7 @@ function emitShortCircuit(arr, jumpOp, markerOp, state) {
2389
2425
  } = state;
2390
2426
  const jumpSlots = [];
2391
2427
  const last = arr.length - 1;
2428
+ bytecode.push(OP_ENTER_SCOPE);
2392
2429
  for (let i = 1; i < last; i++) {
2393
2430
  emitExpression(arr[i], state);
2394
2431
  bytecode.push(jumpOp);
@@ -3363,6 +3400,8 @@ function interpret(compiled, ctx) {
3363
3400
  case OP_POP:
3364
3401
  stackTop$1--;
3365
3402
  break;
3403
+ case OP_ENTER_SCOPE:
3404
+ break;
3366
3405
  case OP_AND:
3367
3406
  case OP_OR:
3368
3407
  case OP_NOR:
@@ -3649,10 +3688,12 @@ let resolvedRefUsedCount = 0;
3649
3688
  // can include it in the reconstructed expression.
3650
3689
  const spillBuf = new Array(MAX_STACK);
3651
3690
  let spillTop = -1;
3652
- // Track the last jump opcode type to detect transitions between short-circuit
3653
- // sequences. Different jump opcodes (41=JUMP_IF_FALSE vs 42=JUMP_IF_TRUE) indicate
3654
- // different short-circuit sequences (AND vs OR/NOR).
3655
- let lastJumpOp = 0;
3691
+ // Scope stack for the spill buffer: each OP_ENTER_SCOPE pushes the current
3692
+ // spillTop so that OP_AND/OR/NOR can drain only the entries added within their
3693
+ // own scope (base+1..spillTop) and restore the outer scope (spillTop = base).
3694
+ // This prevents nested AND/OR from stealing residuals accumulated by outer scopes.
3695
+ const scopeStack = new Array(MAX_STACK);
3696
+ let scopeStackTop = -1;
3656
3697
  function relationalCompare(left, right, op) {
3657
3698
  if (isNumber(left) && isNumber(right)) {
3658
3699
  if (op === OP_GT) {
@@ -3709,7 +3750,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3709
3750
  } = compiled;
3710
3751
  stackTop = -1;
3711
3752
  spillTop = -1;
3712
- lastJumpOp = 0;
3753
+ scopeStackTop = -1;
3713
3754
  let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
3714
3755
  if (overlapRefsResiduals === undefined) {
3715
3756
  overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
@@ -3757,7 +3798,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3757
3798
  case OP_PUSH_REF_DYNAMIC:
3758
3799
  {
3759
3800
  const idx = numAt(bytecode[i++]);
3760
- const rawKey = refRawKeys[idx];
3761
3801
  let val;
3762
3802
  if (resolvedRefDirty[idx]) {
3763
3803
  val = resolvedRefCache[idx];
@@ -3776,12 +3816,12 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3776
3816
  // Ref is absent from context.
3777
3817
  // Check if it should be treated as a concrete undefined (evaluated as undefined)
3778
3818
  // or as a residual expression (preserved for later simplification).
3779
- if (strictSet?.has(rawKey)) {
3819
+ if (strictSet?.has(getKeyFromCompactRef(refs[idx]))) {
3780
3820
  // strictKeys: force-evaluate as undefined (not a residual)
3781
3821
  stack[++stackTop] = undefined;
3782
3822
  break;
3783
3823
  }
3784
- if (optionalSet && !optionalSet.has(rawKey)) {
3824
+ if (optionalSet && !optionalSet.has(getKeyFromCompactRef(refs[idx]))) {
3785
3825
  // Key not in optionalKeys: treat as definitely-present but absent → undefined
3786
3826
  stack[++stackTop] = undefined;
3787
3827
  break;
@@ -4470,13 +4510,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4470
4510
  if (!needsReconstruct(top) && slotVal(top) === false) {
4471
4511
  i += offset;
4472
4512
  }
4473
- // Clear spill buffer when transitioning between short-circuit sequences.
4474
- // Different jump opcodes (41=JUMP_IF_FALSE for AND vs 42=JUMP_IF_TRUE for OR/NOR)
4475
- // indicate different short-circuit sequences.
4476
- if (lastJumpOp !== 41) {
4477
- spillTop = -1;
4478
- }
4479
- lastJumpOp = 41;
4480
4513
  break;
4481
4514
  }
4482
4515
  case OP_JUMP_IF_TRUE:
@@ -4486,13 +4519,11 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4486
4519
  if (!needsReconstruct(top) && slotVal(top) === true) {
4487
4520
  i += offset;
4488
4521
  }
4489
- // Clear spill buffer when transitioning between short-circuit sequences.
4490
- if (lastJumpOp !== 42) {
4491
- spillTop = -1;
4492
- }
4493
- lastJumpOp = 42;
4494
4522
  break;
4495
4523
  }
4524
+ case OP_ENTER_SCOPE:
4525
+ scopeStack[++scopeStackTop] = spillTop;
4526
+ break;
4496
4527
  case OP_POP:
4497
4528
  {
4498
4529
  const popped = stack[stackTop--];
@@ -4507,15 +4538,19 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4507
4538
  // AND/OR/NOR markers — collect the current stack top plus any residuals
4508
4539
  // that were spilled by OP_POP during the short-circuit sequence, then
4509
4540
  // apply the simplification logic.
4541
+ //
4542
+ // Each of these opcodes pops the scope base that was pushed by the
4543
+ // matching OP_ENTER_SCOPE at the start of the short-circuit block. Only
4544
+ // spill entries above that base (indices base+1..spillTop) belong to this
4545
+ // scope; entries at 0..base belong to outer scopes and are preserved by
4546
+ // restoring spillTop = base after draining.
4510
4547
  case OP_AND:
4511
4548
  {
4512
4549
  i++; // consume the operand count byte (unused — we use the spill buffer)
4513
4550
  const top = stack[stackTop--];
4514
- // Fast path: nothing was spilled — all non-top operands were concrete.
4515
- // The top is either a short-circuit false, the last true, or a lone residual.
4516
- // Push top directly — slotVal unwrapping happens at the final return point.
4517
- if (spillTop < 0) {
4518
- spillTop = -1;
4551
+ const base = scopeStack[scopeStackTop--];
4552
+ // Fast path: no entries were spilled in this scope.
4553
+ if (spillTop === base) {
4519
4554
  if (!needsReconstruct(top)) {
4520
4555
  stack[++stackTop] = top;
4521
4556
  } else {
@@ -4524,10 +4559,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4524
4559
  break;
4525
4560
  }
4526
4561
  const residuals = [];
4527
- // Drain spill buffer (residuals from earlier operands that were POP'd)
4528
- // Check if any spilled operand was false (dominates AND)
4529
4562
  let andDominated = false;
4530
- for (let j = 0; j <= spillTop; j++) {
4563
+ for (let j = base + 1; j <= spillTop; j++) {
4531
4564
  const v = spillBuf[j];
4532
4565
  if (!needsReconstruct(v) && slotVal(v) === false) {
4533
4566
  andDominated = true;
@@ -4537,8 +4570,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4537
4570
  residuals.push(slotSrc(v));
4538
4571
  }
4539
4572
  }
4540
- spillTop = -1; // clear spill buffer
4541
- // Include the stack top (last operand result)
4573
+ spillTop = base; // restore outer scope
4542
4574
  if (!andDominated) {
4543
4575
  if (!needsReconstruct(top) && slotVal(top) === false) {
4544
4576
  andDominated = true;
@@ -4561,10 +4593,9 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4561
4593
  {
4562
4594
  i++; // consume the operand count byte
4563
4595
  const top = stack[stackTop--];
4564
- // Fast path: nothing was spilled — all non-top operands were concrete.
4565
- // Push top directly slotVal unwrapping happens at the final return point.
4566
- if (spillTop < 0) {
4567
- spillTop = -1;
4596
+ const base = scopeStack[scopeStackTop--];
4597
+ // Fast path: no entries were spilled in this scope.
4598
+ if (spillTop === base) {
4568
4599
  if (!needsReconstruct(top)) {
4569
4600
  stack[++stackTop] = top;
4570
4601
  } else {
@@ -4574,7 +4605,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4574
4605
  }
4575
4606
  const residuals = [];
4576
4607
  let orDominated = false;
4577
- for (let j = 0; j <= spillTop; j++) {
4608
+ for (let j = base + 1; j <= spillTop; j++) {
4578
4609
  const v = spillBuf[j];
4579
4610
  if (!needsReconstruct(v) && slotVal(v) === true) {
4580
4611
  orDominated = true;
@@ -4584,7 +4615,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4584
4615
  residuals.push(slotSrc(v));
4585
4616
  }
4586
4617
  }
4587
- spillTop = -1;
4618
+ spillTop = base; // restore outer scope
4588
4619
  if (!orDominated) {
4589
4620
  if (!needsReconstruct(top) && slotVal(top) === true) {
4590
4621
  orDominated = true;
@@ -4612,9 +4643,10 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4612
4643
  // OP_NOT will pass through unchanged.
4613
4644
  i++; // consume the operand count byte
4614
4645
  const top = stack[stackTop--];
4646
+ const base = scopeStack[scopeStackTop--];
4615
4647
  const residuals = [];
4616
4648
  let dominated = false;
4617
- for (let j = 0; j <= spillTop; j++) {
4649
+ for (let j = base + 1; j <= spillTop; j++) {
4618
4650
  const v = spillBuf[j];
4619
4651
  if (!needsReconstruct(v) && slotVal(v) === true) {
4620
4652
  dominated = true;
@@ -4624,7 +4656,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4624
4656
  residuals.push(slotSrc(v));
4625
4657
  }
4626
4658
  }
4627
- spillTop = -1;
4659
+ spillTop = base; // restore outer scope
4628
4660
  if (!dominated) {
4629
4661
  if (!needsReconstruct(top) && slotVal(top) === true) {
4630
4662
  dominated = true;
@@ -1806,6 +1806,12 @@ const OP_NOR = 55; // next: N — marker for NOR with N operands
1806
1806
  const OP_IN_SCAN_REFS_CONST = 56;
1807
1807
  // next: N, ref0..refN-1, constIdx — resolve each ref inline, check ∉ consts[constIdx], no stack alloc
1808
1808
  const OP_NOT_IN_SCAN_REFS_CONST = 57;
1809
+ // Marks the entry of a short-circuit AND/OR/NOR scope.
1810
+ // Emitted by the compiler at the start of every emitShortCircuit block.
1811
+ // The evaluate interpreter treats it as a no-op.
1812
+ // The simplify interpreter uses it to push a spill-buffer scope marker so that
1813
+ // nested AND/OR/NOR blocks cannot steal residuals accumulated by an outer scope.
1814
+ const OP_ENTER_SCOPE = 58;
1809
1815
 
1810
1816
  /**
1811
1817
  * Reference path descriptors for the bytecode compiler and interpreter.
@@ -2070,6 +2076,36 @@ function resolveCompactRef(ref, ctx) {
2070
2076
  }
2071
2077
  return resolveTokens(ref.tokens ?? [], ref.t, ctx);
2072
2078
  }
2079
+ function getKeyFromCompactRef(ref) {
2080
+ if (typeof ref === 'string') {
2081
+ return ref;
2082
+ }
2083
+ if (Array.isArray(ref)) {
2084
+ return ref.map(ref => ref.includes('.') ? `\`${ref}\`` : ref).join('.');
2085
+ }
2086
+ let tokens = ref.tokens;
2087
+ if (ref.d) {
2088
+ let current = ref.k;
2089
+ let match = dynamicKeyRegex.exec(current);
2090
+ while (match) {
2091
+ current = current.replace(dynamicKeyRegex, '').replace('[]', '');
2092
+ match = dynamicKeyRegex.exec(current);
2093
+ }
2094
+ tokens = parseStaticKey(current);
2095
+ }
2096
+ let key = '';
2097
+ for (const token of tokens ?? []) {
2098
+ if (isNumber(token.value)) {
2099
+ return key;
2100
+ }
2101
+ if (token.value.includes('.')) {
2102
+ key += `${key ? '.' : ''}\`${token.value}\``;
2103
+ } else {
2104
+ key += `${key ? '.' : ''}${token.value}`;
2105
+ }
2106
+ }
2107
+ return key;
2108
+ }
2073
2109
 
2074
2110
  /**
2075
2111
  * Bytecode compiler.
@@ -2385,6 +2421,7 @@ function emitShortCircuit(arr, jumpOp, markerOp, state) {
2385
2421
  } = state;
2386
2422
  const jumpSlots = [];
2387
2423
  const last = arr.length - 1;
2424
+ bytecode.push(OP_ENTER_SCOPE);
2388
2425
  for (let i = 1; i < last; i++) {
2389
2426
  emitExpression(arr[i], state);
2390
2427
  bytecode.push(jumpOp);
@@ -3359,6 +3396,8 @@ function interpret(compiled, ctx) {
3359
3396
  case OP_POP:
3360
3397
  stackTop$1--;
3361
3398
  break;
3399
+ case OP_ENTER_SCOPE:
3400
+ break;
3362
3401
  case OP_AND:
3363
3402
  case OP_OR:
3364
3403
  case OP_NOR:
@@ -3645,10 +3684,12 @@ let resolvedRefUsedCount = 0;
3645
3684
  // can include it in the reconstructed expression.
3646
3685
  const spillBuf = new Array(MAX_STACK);
3647
3686
  let spillTop = -1;
3648
- // Track the last jump opcode type to detect transitions between short-circuit
3649
- // sequences. Different jump opcodes (41=JUMP_IF_FALSE vs 42=JUMP_IF_TRUE) indicate
3650
- // different short-circuit sequences (AND vs OR/NOR).
3651
- let lastJumpOp = 0;
3687
+ // Scope stack for the spill buffer: each OP_ENTER_SCOPE pushes the current
3688
+ // spillTop so that OP_AND/OR/NOR can drain only the entries added within their
3689
+ // own scope (base+1..spillTop) and restore the outer scope (spillTop = base).
3690
+ // This prevents nested AND/OR from stealing residuals accumulated by outer scopes.
3691
+ const scopeStack = new Array(MAX_STACK);
3692
+ let scopeStackTop = -1;
3652
3693
  function relationalCompare(left, right, op) {
3653
3694
  if (isNumber(left) && isNumber(right)) {
3654
3695
  if (op === OP_GT) {
@@ -3705,7 +3746,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3705
3746
  } = compiled;
3706
3747
  stackTop = -1;
3707
3748
  spillTop = -1;
3708
- lastJumpOp = 0;
3749
+ scopeStackTop = -1;
3709
3750
  let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
3710
3751
  if (overlapRefsResiduals === undefined) {
3711
3752
  overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
@@ -3753,7 +3794,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3753
3794
  case OP_PUSH_REF_DYNAMIC:
3754
3795
  {
3755
3796
  const idx = numAt(bytecode[i++]);
3756
- const rawKey = refRawKeys[idx];
3757
3797
  let val;
3758
3798
  if (resolvedRefDirty[idx]) {
3759
3799
  val = resolvedRefCache[idx];
@@ -3772,12 +3812,12 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3772
3812
  // Ref is absent from context.
3773
3813
  // Check if it should be treated as a concrete undefined (evaluated as undefined)
3774
3814
  // or as a residual expression (preserved for later simplification).
3775
- if (strictSet?.has(rawKey)) {
3815
+ if (strictSet?.has(getKeyFromCompactRef(refs[idx]))) {
3776
3816
  // strictKeys: force-evaluate as undefined (not a residual)
3777
3817
  stack[++stackTop] = undefined;
3778
3818
  break;
3779
3819
  }
3780
- if (optionalSet && !optionalSet.has(rawKey)) {
3820
+ if (optionalSet && !optionalSet.has(getKeyFromCompactRef(refs[idx]))) {
3781
3821
  // Key not in optionalKeys: treat as definitely-present but absent → undefined
3782
3822
  stack[++stackTop] = undefined;
3783
3823
  break;
@@ -4466,13 +4506,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4466
4506
  if (!needsReconstruct(top) && slotVal(top) === false) {
4467
4507
  i += offset;
4468
4508
  }
4469
- // Clear spill buffer when transitioning between short-circuit sequences.
4470
- // Different jump opcodes (41=JUMP_IF_FALSE for AND vs 42=JUMP_IF_TRUE for OR/NOR)
4471
- // indicate different short-circuit sequences.
4472
- if (lastJumpOp !== 41) {
4473
- spillTop = -1;
4474
- }
4475
- lastJumpOp = 41;
4476
4509
  break;
4477
4510
  }
4478
4511
  case OP_JUMP_IF_TRUE:
@@ -4482,13 +4515,11 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4482
4515
  if (!needsReconstruct(top) && slotVal(top) === true) {
4483
4516
  i += offset;
4484
4517
  }
4485
- // Clear spill buffer when transitioning between short-circuit sequences.
4486
- if (lastJumpOp !== 42) {
4487
- spillTop = -1;
4488
- }
4489
- lastJumpOp = 42;
4490
4518
  break;
4491
4519
  }
4520
+ case OP_ENTER_SCOPE:
4521
+ scopeStack[++scopeStackTop] = spillTop;
4522
+ break;
4492
4523
  case OP_POP:
4493
4524
  {
4494
4525
  const popped = stack[stackTop--];
@@ -4503,15 +4534,19 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4503
4534
  // AND/OR/NOR markers — collect the current stack top plus any residuals
4504
4535
  // that were spilled by OP_POP during the short-circuit sequence, then
4505
4536
  // apply the simplification logic.
4537
+ //
4538
+ // Each of these opcodes pops the scope base that was pushed by the
4539
+ // matching OP_ENTER_SCOPE at the start of the short-circuit block. Only
4540
+ // spill entries above that base (indices base+1..spillTop) belong to this
4541
+ // scope; entries at 0..base belong to outer scopes and are preserved by
4542
+ // restoring spillTop = base after draining.
4506
4543
  case OP_AND:
4507
4544
  {
4508
4545
  i++; // consume the operand count byte (unused — we use the spill buffer)
4509
4546
  const top = stack[stackTop--];
4510
- // Fast path: nothing was spilled — all non-top operands were concrete.
4511
- // The top is either a short-circuit false, the last true, or a lone residual.
4512
- // Push top directly — slotVal unwrapping happens at the final return point.
4513
- if (spillTop < 0) {
4514
- spillTop = -1;
4547
+ const base = scopeStack[scopeStackTop--];
4548
+ // Fast path: no entries were spilled in this scope.
4549
+ if (spillTop === base) {
4515
4550
  if (!needsReconstruct(top)) {
4516
4551
  stack[++stackTop] = top;
4517
4552
  } else {
@@ -4520,10 +4555,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4520
4555
  break;
4521
4556
  }
4522
4557
  const residuals = [];
4523
- // Drain spill buffer (residuals from earlier operands that were POP'd)
4524
- // Check if any spilled operand was false (dominates AND)
4525
4558
  let andDominated = false;
4526
- for (let j = 0; j <= spillTop; j++) {
4559
+ for (let j = base + 1; j <= spillTop; j++) {
4527
4560
  const v = spillBuf[j];
4528
4561
  if (!needsReconstruct(v) && slotVal(v) === false) {
4529
4562
  andDominated = true;
@@ -4533,8 +4566,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4533
4566
  residuals.push(slotSrc(v));
4534
4567
  }
4535
4568
  }
4536
- spillTop = -1; // clear spill buffer
4537
- // Include the stack top (last operand result)
4569
+ spillTop = base; // restore outer scope
4538
4570
  if (!andDominated) {
4539
4571
  if (!needsReconstruct(top) && slotVal(top) === false) {
4540
4572
  andDominated = true;
@@ -4557,10 +4589,9 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4557
4589
  {
4558
4590
  i++; // consume the operand count byte
4559
4591
  const top = stack[stackTop--];
4560
- // Fast path: nothing was spilled — all non-top operands were concrete.
4561
- // Push top directly slotVal unwrapping happens at the final return point.
4562
- if (spillTop < 0) {
4563
- spillTop = -1;
4592
+ const base = scopeStack[scopeStackTop--];
4593
+ // Fast path: no entries were spilled in this scope.
4594
+ if (spillTop === base) {
4564
4595
  if (!needsReconstruct(top)) {
4565
4596
  stack[++stackTop] = top;
4566
4597
  } else {
@@ -4570,7 +4601,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4570
4601
  }
4571
4602
  const residuals = [];
4572
4603
  let orDominated = false;
4573
- for (let j = 0; j <= spillTop; j++) {
4604
+ for (let j = base + 1; j <= spillTop; j++) {
4574
4605
  const v = spillBuf[j];
4575
4606
  if (!needsReconstruct(v) && slotVal(v) === true) {
4576
4607
  orDominated = true;
@@ -4580,7 +4611,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4580
4611
  residuals.push(slotSrc(v));
4581
4612
  }
4582
4613
  }
4583
- spillTop = -1;
4614
+ spillTop = base; // restore outer scope
4584
4615
  if (!orDominated) {
4585
4616
  if (!needsReconstruct(top) && slotVal(top) === true) {
4586
4617
  orDominated = true;
@@ -4608,9 +4639,10 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4608
4639
  // OP_NOT will pass through unchanged.
4609
4640
  i++; // consume the operand count byte
4610
4641
  const top = stack[stackTop--];
4642
+ const base = scopeStack[scopeStackTop--];
4611
4643
  const residuals = [];
4612
4644
  let dominated = false;
4613
- for (let j = 0; j <= spillTop; j++) {
4645
+ for (let j = base + 1; j <= spillTop; j++) {
4614
4646
  const v = spillBuf[j];
4615
4647
  if (!needsReconstruct(v) && slotVal(v) === true) {
4616
4648
  dominated = true;
@@ -4620,7 +4652,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4620
4652
  residuals.push(slotSrc(v));
4621
4653
  }
4622
4654
  }
4623
- spillTop = -1;
4655
+ spillTop = base; // restore outer scope
4624
4656
  if (!dominated) {
4625
4657
  if (!needsReconstruct(top) && slotVal(top) === true) {
4626
4658
  dominated = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@briza/illogical",
3
- "version": "2.2.0",
3
+ "version": "2.2.2",
4
4
  "description": "A micro conditional javascript engine used to parse the raw logical and comparison expressions, evaluate the expression in the given data context, and provide access to a text form of the given expressions.",
5
5
  "type": "module",
6
6
  "main": "./lib/illogical.cjs",
@@ -15,8 +15,8 @@
15
15
  "types/"
16
16
  ],
17
17
  "author": {
18
- "name": "David Horak",
19
- "email": "dhorak@briza.com"
18
+ "name": "Briza, Inc.",
19
+ "email": "dev@briza.com"
20
20
  },
21
21
  "license": "MIT",
22
22
  "scripts": {
package/readme.md CHANGED
@@ -9,32 +9,32 @@
9
9
  <h3 align="center">illogical</h3>
10
10
  </div>
11
11
 
12
- [![build status](https://github.com/briza-insurance/illogical/actions/workflows/test.yml/badge.svg)](https://github.com/briza-insurance/illogical/actions?branch=master)
13
- [![npm version](https://badge.fury.io/js/@briza%2Fillogical.svg?icon=si%3Anpm)](https://badge.fury.io/js/@briza%2Fillogical)
14
- [![install size](https://packagephobia.com/badge?p=@briza/illogical)](https://packagephobia.com/result?p=@briza/illogical)
15
- ![zero dependencies](https://img.shields.io/badge/0-dependencies-green)
16
- ![npm downloads](https://img.shields.io/npm/dm/%40briza%2Fillogical)
17
-
18
- </div>
19
-
20
- <div align="center">
21
- <p>
22
- <br />
23
- JSON DSL for expressing and evaluating business rules. Underwriters use illogical to model business rules for their question sets, enabling distributors to render great user experiences.
24
- </p>
12
+ <p>
13
+ <strong>illogical</strong> is a JSON DSL (domain-specific language) for expressing and evaluating business rules in the insurance industry. Underwriters use illogical to model business rules for their question sets, enabling distributors to render great user experiences.
14
+ </p>
15
+
16
+ [![build status](https://github.com/briza-insurance/illogical/actions/workflows/test.yml/badge.svg)](https://github.com/briza-insurance/illogical/actions?branch=master)
17
+ [![npm version](https://badge.fury.io/js/@briza%2Fillogical.svg?icon=si%3Anpm)](https://badge.fury.io/js/@briza%2Fillogical)
18
+ [![install size](https://packagephobia.com/badge?p=@briza/illogical)](https://packagephobia.com/result?p=@briza/illogical)
19
+ ![zero dependencies](https://img.shields.io/badge/0-dependencies-green)
20
+ ![npm downloads](https://img.shields.io/npm/dm/%40briza%2Fillogical)
25
21
  </div>
26
22
 
27
23
  ---
28
24
 
29
- This project is designed such that business rules can be shared between the front-end and back-end, serialized in JSON.
25
+ ## 🚀 Getting Started
26
+
27
+ Get up and running with illogical in just a few steps.
30
28
 
31
- ## 🚀 Get Started
29
+ ### Installation
32
30
 
33
31
  ```sh
34
32
  # install illogical
35
33
  npm install @briza/illogical
36
34
  ```
37
35
 
36
+ ### Basic Usage
37
+
38
38
  ```js
39
39
  // Import the illogical engine
40
40
  import Engine from '@briza/illogical'
@@ -65,9 +65,11 @@ engine.evaluate(['==', '$age.(String)', '21'], ctx) // true
65
65
  engine.evaluate(['AND', ['>', '$age', 20], ['==', '$name', 'peter']]) // true
66
66
  ```
67
67
 
68
- ## 🖼️ Resources
68
+ ## 📚 Documentation
69
+
70
+ ### Core Concepts
69
71
 
70
- Understand supported expressions:
72
+ Explore the supported expressions and their usage:
71
73
 
72
74
  - [Comparison Expressions](./specs/comparison-expressions.md)
73
75
  - [Logical Expressions](./specs/logical-expressions.md)
@@ -75,19 +77,25 @@ Understand supported expressions:
75
77
  - [Evaluation Data Context](./specs/evaluation-data-context.md)
76
78
  - [Operand Types](./specs/operand-types.md)
77
79
 
78
- Learn about usages:
80
+ ### API Reference
81
+
82
+ Learn how to use the engine and its methods:
79
83
 
80
84
  - [Evaluate](./specs/evaluate.md)
81
85
  - [Statement](./specs/statement.md)
82
86
  - [Parse](./specs/parse.md)
83
87
  - [Simplify](./specs/simplify.md)
84
88
 
89
+ ### Customization
90
+
85
91
  Customize the engine and the documentation:
86
92
 
87
93
  - [Engine Options](./specs/engine.md)
88
94
  - [Code Documentation](https://briza-insurance.github.io/illogical/index.html)
89
95
 
90
- Bytecode evaluation and debugging tools:
96
+ ### Development Tools
97
+
98
+ For advanced usage like bytecode evaluation and debugging:
91
99
 
92
100
  - [Bytecode Evaluator](./specs/bytecode-evaluator.md)
93
101
  - [Debugger Tools](./specs/debugger-tools.md)
@@ -49,3 +49,4 @@ export declare const OP_OR = 54;
49
49
  export declare const OP_NOR = 55;
50
50
  export declare const OP_IN_SCAN_REFS_CONST = 56;
51
51
  export declare const OP_NOT_IN_SCAN_REFS_CONST = 57;
52
+ export declare const OP_ENTER_SCOPE = 58;
@@ -71,3 +71,4 @@ export declare function resolveDynamic(key: string, dataType: DataType | undefin
71
71
  * (OP_OVERLAP_SCAN_REFS_CONST, OP_OR_AND_IN_CONST_2).
72
72
  */
73
73
  export declare function resolveCompactRef(ref: CompactRef, ctx: Context): Result;
74
+ export declare function getKeyFromCompactRef(ref: CompactRef): string;