@briza/illogical 2.2.1 → 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.
@@ -2419,6 +2425,7 @@ function emitShortCircuit(arr, jumpOp, markerOp, state) {
2419
2425
  } = state;
2420
2426
  const jumpSlots = [];
2421
2427
  const last = arr.length - 1;
2428
+ bytecode.push(OP_ENTER_SCOPE);
2422
2429
  for (let i = 1; i < last; i++) {
2423
2430
  emitExpression(arr[i], state);
2424
2431
  bytecode.push(jumpOp);
@@ -3393,6 +3400,8 @@ function interpret(compiled, ctx) {
3393
3400
  case OP_POP:
3394
3401
  stackTop$1--;
3395
3402
  break;
3403
+ case OP_ENTER_SCOPE:
3404
+ break;
3396
3405
  case OP_AND:
3397
3406
  case OP_OR:
3398
3407
  case OP_NOR:
@@ -3679,10 +3688,12 @@ let resolvedRefUsedCount = 0;
3679
3688
  // can include it in the reconstructed expression.
3680
3689
  const spillBuf = new Array(MAX_STACK);
3681
3690
  let spillTop = -1;
3682
- // Track the last jump opcode type to detect transitions between short-circuit
3683
- // sequences. Different jump opcodes (41=JUMP_IF_FALSE vs 42=JUMP_IF_TRUE) indicate
3684
- // different short-circuit sequences (AND vs OR/NOR).
3685
- 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;
3686
3697
  function relationalCompare(left, right, op) {
3687
3698
  if (isNumber(left) && isNumber(right)) {
3688
3699
  if (op === OP_GT) {
@@ -3739,7 +3750,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3739
3750
  } = compiled;
3740
3751
  stackTop = -1;
3741
3752
  spillTop = -1;
3742
- lastJumpOp = 0;
3753
+ scopeStackTop = -1;
3743
3754
  let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
3744
3755
  if (overlapRefsResiduals === undefined) {
3745
3756
  overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
@@ -4499,13 +4510,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4499
4510
  if (!needsReconstruct(top) && slotVal(top) === false) {
4500
4511
  i += offset;
4501
4512
  }
4502
- // Clear spill buffer when transitioning between short-circuit sequences.
4503
- // Different jump opcodes (41=JUMP_IF_FALSE for AND vs 42=JUMP_IF_TRUE for OR/NOR)
4504
- // indicate different short-circuit sequences.
4505
- if (lastJumpOp !== 41) {
4506
- spillTop = -1;
4507
- }
4508
- lastJumpOp = 41;
4509
4513
  break;
4510
4514
  }
4511
4515
  case OP_JUMP_IF_TRUE:
@@ -4515,13 +4519,11 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4515
4519
  if (!needsReconstruct(top) && slotVal(top) === true) {
4516
4520
  i += offset;
4517
4521
  }
4518
- // Clear spill buffer when transitioning between short-circuit sequences.
4519
- if (lastJumpOp !== 42) {
4520
- spillTop = -1;
4521
- }
4522
- lastJumpOp = 42;
4523
4522
  break;
4524
4523
  }
4524
+ case OP_ENTER_SCOPE:
4525
+ scopeStack[++scopeStackTop] = spillTop;
4526
+ break;
4525
4527
  case OP_POP:
4526
4528
  {
4527
4529
  const popped = stack[stackTop--];
@@ -4536,15 +4538,19 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4536
4538
  // AND/OR/NOR markers — collect the current stack top plus any residuals
4537
4539
  // that were spilled by OP_POP during the short-circuit sequence, then
4538
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.
4539
4547
  case OP_AND:
4540
4548
  {
4541
4549
  i++; // consume the operand count byte (unused — we use the spill buffer)
4542
4550
  const top = stack[stackTop--];
4543
- // Fast path: nothing was spilled — all non-top operands were concrete.
4544
- // The top is either a short-circuit false, the last true, or a lone residual.
4545
- // Push top directly — slotVal unwrapping happens at the final return point.
4546
- if (spillTop < 0) {
4547
- spillTop = -1;
4551
+ const base = scopeStack[scopeStackTop--];
4552
+ // Fast path: no entries were spilled in this scope.
4553
+ if (spillTop === base) {
4548
4554
  if (!needsReconstruct(top)) {
4549
4555
  stack[++stackTop] = top;
4550
4556
  } else {
@@ -4553,10 +4559,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4553
4559
  break;
4554
4560
  }
4555
4561
  const residuals = [];
4556
- // Drain spill buffer (residuals from earlier operands that were POP'd)
4557
- // Check if any spilled operand was false (dominates AND)
4558
4562
  let andDominated = false;
4559
- for (let j = 0; j <= spillTop; j++) {
4563
+ for (let j = base + 1; j <= spillTop; j++) {
4560
4564
  const v = spillBuf[j];
4561
4565
  if (!needsReconstruct(v) && slotVal(v) === false) {
4562
4566
  andDominated = true;
@@ -4566,8 +4570,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4566
4570
  residuals.push(slotSrc(v));
4567
4571
  }
4568
4572
  }
4569
- spillTop = -1; // clear spill buffer
4570
- // Include the stack top (last operand result)
4573
+ spillTop = base; // restore outer scope
4571
4574
  if (!andDominated) {
4572
4575
  if (!needsReconstruct(top) && slotVal(top) === false) {
4573
4576
  andDominated = true;
@@ -4590,10 +4593,9 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4590
4593
  {
4591
4594
  i++; // consume the operand count byte
4592
4595
  const top = stack[stackTop--];
4593
- // Fast path: nothing was spilled — all non-top operands were concrete.
4594
- // Push top directly slotVal unwrapping happens at the final return point.
4595
- if (spillTop < 0) {
4596
- spillTop = -1;
4596
+ const base = scopeStack[scopeStackTop--];
4597
+ // Fast path: no entries were spilled in this scope.
4598
+ if (spillTop === base) {
4597
4599
  if (!needsReconstruct(top)) {
4598
4600
  stack[++stackTop] = top;
4599
4601
  } else {
@@ -4603,7 +4605,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4603
4605
  }
4604
4606
  const residuals = [];
4605
4607
  let orDominated = false;
4606
- for (let j = 0; j <= spillTop; j++) {
4608
+ for (let j = base + 1; j <= spillTop; j++) {
4607
4609
  const v = spillBuf[j];
4608
4610
  if (!needsReconstruct(v) && slotVal(v) === true) {
4609
4611
  orDominated = true;
@@ -4613,7 +4615,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4613
4615
  residuals.push(slotSrc(v));
4614
4616
  }
4615
4617
  }
4616
- spillTop = -1;
4618
+ spillTop = base; // restore outer scope
4617
4619
  if (!orDominated) {
4618
4620
  if (!needsReconstruct(top) && slotVal(top) === true) {
4619
4621
  orDominated = true;
@@ -4641,9 +4643,10 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4641
4643
  // OP_NOT will pass through unchanged.
4642
4644
  i++; // consume the operand count byte
4643
4645
  const top = stack[stackTop--];
4646
+ const base = scopeStack[scopeStackTop--];
4644
4647
  const residuals = [];
4645
4648
  let dominated = false;
4646
- for (let j = 0; j <= spillTop; j++) {
4649
+ for (let j = base + 1; j <= spillTop; j++) {
4647
4650
  const v = spillBuf[j];
4648
4651
  if (!needsReconstruct(v) && slotVal(v) === true) {
4649
4652
  dominated = true;
@@ -4653,7 +4656,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4653
4656
  residuals.push(slotSrc(v));
4654
4657
  }
4655
4658
  }
4656
- spillTop = -1;
4659
+ spillTop = base; // restore outer scope
4657
4660
  if (!dominated) {
4658
4661
  if (!needsReconstruct(top) && slotVal(top) === true) {
4659
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.
@@ -2415,6 +2421,7 @@ function emitShortCircuit(arr, jumpOp, markerOp, state) {
2415
2421
  } = state;
2416
2422
  const jumpSlots = [];
2417
2423
  const last = arr.length - 1;
2424
+ bytecode.push(OP_ENTER_SCOPE);
2418
2425
  for (let i = 1; i < last; i++) {
2419
2426
  emitExpression(arr[i], state);
2420
2427
  bytecode.push(jumpOp);
@@ -3389,6 +3396,8 @@ function interpret(compiled, ctx) {
3389
3396
  case OP_POP:
3390
3397
  stackTop$1--;
3391
3398
  break;
3399
+ case OP_ENTER_SCOPE:
3400
+ break;
3392
3401
  case OP_AND:
3393
3402
  case OP_OR:
3394
3403
  case OP_NOR:
@@ -3675,10 +3684,12 @@ let resolvedRefUsedCount = 0;
3675
3684
  // can include it in the reconstructed expression.
3676
3685
  const spillBuf = new Array(MAX_STACK);
3677
3686
  let spillTop = -1;
3678
- // Track the last jump opcode type to detect transitions between short-circuit
3679
- // sequences. Different jump opcodes (41=JUMP_IF_FALSE vs 42=JUMP_IF_TRUE) indicate
3680
- // different short-circuit sequences (AND vs OR/NOR).
3681
- 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;
3682
3693
  function relationalCompare(left, right, op) {
3683
3694
  if (isNumber(left) && isNumber(right)) {
3684
3695
  if (op === OP_GT) {
@@ -3735,7 +3746,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
3735
3746
  } = compiled;
3736
3747
  stackTop = -1;
3737
3748
  spillTop = -1;
3738
- lastJumpOp = 0;
3749
+ scopeStackTop = -1;
3739
3750
  let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
3740
3751
  if (overlapRefsResiduals === undefined) {
3741
3752
  overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
@@ -4495,13 +4506,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4495
4506
  if (!needsReconstruct(top) && slotVal(top) === false) {
4496
4507
  i += offset;
4497
4508
  }
4498
- // Clear spill buffer when transitioning between short-circuit sequences.
4499
- // Different jump opcodes (41=JUMP_IF_FALSE for AND vs 42=JUMP_IF_TRUE for OR/NOR)
4500
- // indicate different short-circuit sequences.
4501
- if (lastJumpOp !== 41) {
4502
- spillTop = -1;
4503
- }
4504
- lastJumpOp = 41;
4505
4509
  break;
4506
4510
  }
4507
4511
  case OP_JUMP_IF_TRUE:
@@ -4511,13 +4515,11 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4511
4515
  if (!needsReconstruct(top) && slotVal(top) === true) {
4512
4516
  i += offset;
4513
4517
  }
4514
- // Clear spill buffer when transitioning between short-circuit sequences.
4515
- if (lastJumpOp !== 42) {
4516
- spillTop = -1;
4517
- }
4518
- lastJumpOp = 42;
4519
4518
  break;
4520
4519
  }
4520
+ case OP_ENTER_SCOPE:
4521
+ scopeStack[++scopeStackTop] = spillTop;
4522
+ break;
4521
4523
  case OP_POP:
4522
4524
  {
4523
4525
  const popped = stack[stackTop--];
@@ -4532,15 +4534,19 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4532
4534
  // AND/OR/NOR markers — collect the current stack top plus any residuals
4533
4535
  // that were spilled by OP_POP during the short-circuit sequence, then
4534
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.
4535
4543
  case OP_AND:
4536
4544
  {
4537
4545
  i++; // consume the operand count byte (unused — we use the spill buffer)
4538
4546
  const top = stack[stackTop--];
4539
- // Fast path: nothing was spilled — all non-top operands were concrete.
4540
- // The top is either a short-circuit false, the last true, or a lone residual.
4541
- // Push top directly — slotVal unwrapping happens at the final return point.
4542
- if (spillTop < 0) {
4543
- spillTop = -1;
4547
+ const base = scopeStack[scopeStackTop--];
4548
+ // Fast path: no entries were spilled in this scope.
4549
+ if (spillTop === base) {
4544
4550
  if (!needsReconstruct(top)) {
4545
4551
  stack[++stackTop] = top;
4546
4552
  } else {
@@ -4549,10 +4555,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4549
4555
  break;
4550
4556
  }
4551
4557
  const residuals = [];
4552
- // Drain spill buffer (residuals from earlier operands that were POP'd)
4553
- // Check if any spilled operand was false (dominates AND)
4554
4558
  let andDominated = false;
4555
- for (let j = 0; j <= spillTop; j++) {
4559
+ for (let j = base + 1; j <= spillTop; j++) {
4556
4560
  const v = spillBuf[j];
4557
4561
  if (!needsReconstruct(v) && slotVal(v) === false) {
4558
4562
  andDominated = true;
@@ -4562,8 +4566,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4562
4566
  residuals.push(slotSrc(v));
4563
4567
  }
4564
4568
  }
4565
- spillTop = -1; // clear spill buffer
4566
- // Include the stack top (last operand result)
4569
+ spillTop = base; // restore outer scope
4567
4570
  if (!andDominated) {
4568
4571
  if (!needsReconstruct(top) && slotVal(top) === false) {
4569
4572
  andDominated = true;
@@ -4586,10 +4589,9 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4586
4589
  {
4587
4590
  i++; // consume the operand count byte
4588
4591
  const top = stack[stackTop--];
4589
- // Fast path: nothing was spilled — all non-top operands were concrete.
4590
- // Push top directly slotVal unwrapping happens at the final return point.
4591
- if (spillTop < 0) {
4592
- spillTop = -1;
4592
+ const base = scopeStack[scopeStackTop--];
4593
+ // Fast path: no entries were spilled in this scope.
4594
+ if (spillTop === base) {
4593
4595
  if (!needsReconstruct(top)) {
4594
4596
  stack[++stackTop] = top;
4595
4597
  } else {
@@ -4599,7 +4601,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4599
4601
  }
4600
4602
  const residuals = [];
4601
4603
  let orDominated = false;
4602
- for (let j = 0; j <= spillTop; j++) {
4604
+ for (let j = base + 1; j <= spillTop; j++) {
4603
4605
  const v = spillBuf[j];
4604
4606
  if (!needsReconstruct(v) && slotVal(v) === true) {
4605
4607
  orDominated = true;
@@ -4609,7 +4611,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4609
4611
  residuals.push(slotSrc(v));
4610
4612
  }
4611
4613
  }
4612
- spillTop = -1;
4614
+ spillTop = base; // restore outer scope
4613
4615
  if (!orDominated) {
4614
4616
  if (!needsReconstruct(top) && slotVal(top) === true) {
4615
4617
  orDominated = true;
@@ -4637,9 +4639,10 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4637
4639
  // OP_NOT will pass through unchanged.
4638
4640
  i++; // consume the operand count byte
4639
4641
  const top = stack[stackTop--];
4642
+ const base = scopeStack[scopeStackTop--];
4640
4643
  const residuals = [];
4641
4644
  let dominated = false;
4642
- for (let j = 0; j <= spillTop; j++) {
4645
+ for (let j = base + 1; j <= spillTop; j++) {
4643
4646
  const v = spillBuf[j];
4644
4647
  if (!needsReconstruct(v) && slotVal(v) === true) {
4645
4648
  dominated = true;
@@ -4649,7 +4652,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
4649
4652
  residuals.push(slotSrc(v));
4650
4653
  }
4651
4654
  }
4652
- spillTop = -1;
4655
+ spillTop = base; // restore outer scope
4653
4656
  if (!dominated) {
4654
4657
  if (!needsReconstruct(top) && slotVal(top) === true) {
4655
4658
  dominated = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@briza/illogical",
3
- "version": "2.2.1",
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
@@ -8,15 +8,11 @@
8
8
  <div align="center">
9
9
  <h3 align="center">illogical</h3>
10
10
  </div>
11
- </div>
12
11
 
13
- <div align="center">
14
12
  <p>
15
- **illogical** 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.
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.
16
14
  </p>
17
- </div>
18
15
 
19
- <div align="center">
20
16
  [![build status](https://github.com/briza-insurance/illogical/actions/workflows/test.yml/badge.svg)](https://github.com/briza-insurance/illogical/actions?branch=master)
21
17
  [![npm version](https://badge.fury.io/js/@briza%2Fillogical.svg?icon=si%3Anpm)](https://badge.fury.io/js/@briza%2Fillogical)
22
18
  [![install size](https://packagephobia.com/badge?p=@briza/illogical)](https://packagephobia.com/result?p=@briza/illogical)
@@ -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;