@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 +70 -38
- package/lib/illogical.esm.js +70 -38
- package/package.json +3 -3
- package/readme.md +27 -19
- package/types/bytecode/opcodes.d.ts +1 -0
- package/types/bytecode/refs.d.ts +1 -0
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
|
-
//
|
|
3653
|
-
//
|
|
3654
|
-
//
|
|
3655
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
4515
|
-
//
|
|
4516
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4565
|
-
//
|
|
4566
|
-
if (spillTop
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4659
|
+
spillTop = base; // restore outer scope
|
|
4628
4660
|
if (!dominated) {
|
|
4629
4661
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4630
4662
|
dominated = true;
|
package/lib/illogical.esm.js
CHANGED
|
@@ -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
|
-
//
|
|
3649
|
-
//
|
|
3650
|
-
//
|
|
3651
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
-
|
|
4511
|
-
//
|
|
4512
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
4561
|
-
//
|
|
4562
|
-
if (spillTop
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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.
|
|
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": "
|
|
19
|
-
"email": "
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
](https://github.com/briza-insurance/illogical/actions?branch=master)
|
|
17
|
+
[](https://badge.fury.io/js/@briza%2Fillogical)
|
|
18
|
+
[](https://packagephobia.com/result?p=@briza/illogical)
|
|
19
|
+

|
|
20
|
+

|
|
25
21
|
</div>
|
|
26
22
|
|
|
27
23
|
---
|
|
28
24
|
|
|
29
|
-
|
|
25
|
+
## 🚀 Getting Started
|
|
26
|
+
|
|
27
|
+
Get up and running with illogical in just a few steps.
|
|
30
28
|
|
|
31
|
-
|
|
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
|
-
##
|
|
68
|
+
## 📚 Documentation
|
|
69
|
+
|
|
70
|
+
### Core Concepts
|
|
69
71
|
|
|
70
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/types/bytecode/refs.d.ts
CHANGED
|
@@ -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;
|