@briza/illogical 2.2.1 → 2.2.3
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 +158 -57
- package/lib/illogical.esm.js +158 -57
- package/package.json +3 -3
- package/readme.md +8 -10
- package/types/bytecode/opcodes.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.
|
|
@@ -2287,7 +2293,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2287
2293
|
return {
|
|
2288
2294
|
rawRef,
|
|
2289
2295
|
refKey,
|
|
2290
|
-
vals: right
|
|
2296
|
+
vals: right,
|
|
2297
|
+
operator: 'in'
|
|
2291
2298
|
};
|
|
2292
2299
|
}
|
|
2293
2300
|
|
|
@@ -2303,7 +2310,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2303
2310
|
return {
|
|
2304
2311
|
rawRef,
|
|
2305
2312
|
refKey,
|
|
2306
|
-
vals: [right]
|
|
2313
|
+
vals: [right],
|
|
2314
|
+
operator: 'eq'
|
|
2307
2315
|
};
|
|
2308
2316
|
}
|
|
2309
2317
|
return null;
|
|
@@ -2335,6 +2343,10 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2335
2343
|
|
|
2336
2344
|
// Inverted index: each distinct setA value → merged setB values across all branches containing it
|
|
2337
2345
|
const setBValsByAValue = new Map();
|
|
2346
|
+
// Track which operators were used for each setA value (for ref1 operand reconstruction)
|
|
2347
|
+
const ref1OpsByAValue = new Map();
|
|
2348
|
+
// Track which operators were used for each setA value (for ref2 operand reconstruction)
|
|
2349
|
+
const ref2OpsByAValue = new Map();
|
|
2338
2350
|
for (let b = 1; b <= nBranches; b++) {
|
|
2339
2351
|
const branch = arr[b];
|
|
2340
2352
|
if (!Array.isArray(branch)) {
|
|
@@ -2390,6 +2402,20 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2390
2402
|
setBVals = new Set();
|
|
2391
2403
|
setBValsByAValue.set(aVal, setBVals);
|
|
2392
2404
|
}
|
|
2405
|
+
// Track ref1 operator
|
|
2406
|
+
let r1Ops = ref1OpsByAValue.get(aVal);
|
|
2407
|
+
if (r1Ops === undefined) {
|
|
2408
|
+
r1Ops = new Set();
|
|
2409
|
+
ref1OpsByAValue.set(aVal, r1Ops);
|
|
2410
|
+
}
|
|
2411
|
+
r1Ops.add(extA.operator);
|
|
2412
|
+
// Track ref2 operator
|
|
2413
|
+
let r2Ops = ref2OpsByAValue.get(aVal);
|
|
2414
|
+
if (r2Ops === undefined) {
|
|
2415
|
+
r2Ops = new Set();
|
|
2416
|
+
ref2OpsByAValue.set(aVal, r2Ops);
|
|
2417
|
+
}
|
|
2418
|
+
r2Ops.add(extB.operator);
|
|
2393
2419
|
for (const bVal of extB.vals) {
|
|
2394
2420
|
setBVals.add(bVal);
|
|
2395
2421
|
}
|
|
@@ -2401,14 +2427,25 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2401
2427
|
|
|
2402
2428
|
// Build entries: one (literal aVal, mergedSetBIdx) per distinct setA value
|
|
2403
2429
|
const entries = [];
|
|
2430
|
+
// Track operators per entry: [ref1Op, ref2Op]
|
|
2431
|
+
const entryOperators = [];
|
|
2404
2432
|
for (const [aVal, setBVals] of setBValsByAValue) {
|
|
2405
2433
|
const mergedSetB = [...setBVals].filter(v => v !== undefined);
|
|
2406
|
-
|
|
2434
|
+
const constIdx = internConst(mergedSetB, state);
|
|
2435
|
+
entries.push([aVal, constIdx]);
|
|
2436
|
+
// Determine ref1 operator: if all branches used 'eq', preserve 'eq'; otherwise use 'in'
|
|
2437
|
+
const r1Ops = ref1OpsByAValue.get(aVal);
|
|
2438
|
+
const ref1Op = r1Ops !== undefined && r1Ops.size === 1 && r1Ops.has('eq') ? 'eq' : 'in';
|
|
2439
|
+
// Determine ref2 operator: if all branches used 'eq', preserve 'eq'; otherwise use 'in'
|
|
2440
|
+
const r2Ops = ref2OpsByAValue.get(aVal);
|
|
2441
|
+
const ref2Op = r2Ops !== undefined && r2Ops.size === 1 && r2Ops.has('eq') ? 'eq' : 'in';
|
|
2442
|
+
entryOperators.push([ref1Op, ref2Op]);
|
|
2407
2443
|
}
|
|
2408
2444
|
return {
|
|
2409
2445
|
ref1Raw,
|
|
2410
2446
|
ref2Raw,
|
|
2411
|
-
entries
|
|
2447
|
+
entries,
|
|
2448
|
+
entryOperators
|
|
2412
2449
|
};
|
|
2413
2450
|
}
|
|
2414
2451
|
|
|
@@ -2419,6 +2456,7 @@ function emitShortCircuit(arr, jumpOp, markerOp, state) {
|
|
|
2419
2456
|
} = state;
|
|
2420
2457
|
const jumpSlots = [];
|
|
2421
2458
|
const last = arr.length - 1;
|
|
2459
|
+
bytecode.push(OP_ENTER_SCOPE);
|
|
2422
2460
|
for (let i = 1; i < last; i++) {
|
|
2423
2461
|
emitExpression(arr[i], state);
|
|
2424
2462
|
bytecode.push(jumpOp);
|
|
@@ -2465,18 +2503,22 @@ function emitExpression(raw, state) {
|
|
|
2465
2503
|
const {
|
|
2466
2504
|
ref1Raw,
|
|
2467
2505
|
ref2Raw,
|
|
2468
|
-
entries
|
|
2506
|
+
entries,
|
|
2507
|
+
entryOperators
|
|
2469
2508
|
} = orAnd2;
|
|
2470
2509
|
const {
|
|
2471
2510
|
bytecode
|
|
2472
2511
|
} = state;
|
|
2473
2512
|
const ref1Idx = internRef(ref1Raw, state);
|
|
2474
2513
|
const ref2Idx = internRef(ref2Raw, state);
|
|
2475
|
-
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M
|
|
2514
|
+
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M (aVal0 setBIdx0 ref1Op0 ref2Op0) ...
|
|
2476
2515
|
// M is the number of distinct setA values across all branches (after inverted-index merge).
|
|
2516
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
2477
2517
|
bytecode.push(OP_OR_AND_IN_CONST_2, ref1Idx, ref2Idx, entries.length);
|
|
2478
|
-
for (
|
|
2479
|
-
|
|
2518
|
+
for (let j = 0; j < entries.length; j++) {
|
|
2519
|
+
const [aVal, mergedSetBIdx] = entries[j];
|
|
2520
|
+
const [ref1Op, ref2Op] = entryOperators[j];
|
|
2521
|
+
bytecode.push(aVal, mergedSetBIdx, ref1Op === 'in' ? 1 : 0, ref2Op === 'in' ? 1 : 0);
|
|
2480
2522
|
}
|
|
2481
2523
|
return;
|
|
2482
2524
|
}
|
|
@@ -3208,20 +3250,21 @@ function interpret(compiled, ctx) {
|
|
|
3208
3250
|
}
|
|
3209
3251
|
case OP_OR_AND_IN_CONST_2:
|
|
3210
3252
|
{
|
|
3211
|
-
// bytecode layout: ref1Idx, ref2Idx, M, v0, setBIdx0, v1, setBIdx1,
|
|
3253
|
+
// bytecode layout: ref1Idx, ref2Idx, M, (v0, setBIdx0, ref1Op0, ref2Op0), (v1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
3254
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in' (unused at runtime, kept for simplifier)
|
|
3212
3255
|
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3213
3256
|
const ref1Idx = numAt$1(bytecode[++i]);
|
|
3214
3257
|
const ref2Idx = numAt$1(bytecode[++i]);
|
|
3215
3258
|
const m = numAt$1(bytecode[++i]);
|
|
3216
3259
|
const entriesStart = i + 1;
|
|
3217
|
-
i += m *
|
|
3260
|
+
i += m * 4;
|
|
3218
3261
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
3219
3262
|
const v2 = resolveCompactRef(refs[ref2Idx], ctx);
|
|
3220
3263
|
let found = false;
|
|
3221
3264
|
if (v1 !== undefined && v1 !== null && v2 !== undefined && v2 !== null) {
|
|
3222
3265
|
for (let j = 0; j < m; j++) {
|
|
3223
|
-
if (bytecode[entriesStart + j *
|
|
3224
|
-
found = constSets[numAt$1(bytecode[entriesStart + j *
|
|
3266
|
+
if (bytecode[entriesStart + j * 4] === v1) {
|
|
3267
|
+
found = constSets[numAt$1(bytecode[entriesStart + j * 4 + 1])].has(v2);
|
|
3225
3268
|
break;
|
|
3226
3269
|
}
|
|
3227
3270
|
}
|
|
@@ -3393,6 +3436,8 @@ function interpret(compiled, ctx) {
|
|
|
3393
3436
|
case OP_POP:
|
|
3394
3437
|
stackTop$1--;
|
|
3395
3438
|
break;
|
|
3439
|
+
case OP_ENTER_SCOPE:
|
|
3440
|
+
break;
|
|
3396
3441
|
case OP_AND:
|
|
3397
3442
|
case OP_OR:
|
|
3398
3443
|
case OP_NOR:
|
|
@@ -3679,10 +3724,12 @@ let resolvedRefUsedCount = 0;
|
|
|
3679
3724
|
// can include it in the reconstructed expression.
|
|
3680
3725
|
const spillBuf = new Array(MAX_STACK);
|
|
3681
3726
|
let spillTop = -1;
|
|
3682
|
-
//
|
|
3683
|
-
//
|
|
3684
|
-
//
|
|
3685
|
-
|
|
3727
|
+
// Scope stack for the spill buffer: each OP_ENTER_SCOPE pushes the current
|
|
3728
|
+
// spillTop so that OP_AND/OR/NOR can drain only the entries added within their
|
|
3729
|
+
// own scope (base+1..spillTop) and restore the outer scope (spillTop = base).
|
|
3730
|
+
// This prevents nested AND/OR from stealing residuals accumulated by outer scopes.
|
|
3731
|
+
const scopeStack = new Array(MAX_STACK);
|
|
3732
|
+
let scopeStackTop = -1;
|
|
3686
3733
|
function relationalCompare(left, right, op) {
|
|
3687
3734
|
if (isNumber(left) && isNumber(right)) {
|
|
3688
3735
|
if (op === OP_GT) {
|
|
@@ -3739,7 +3786,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
3739
3786
|
} = compiled;
|
|
3740
3787
|
stackTop = -1;
|
|
3741
3788
|
spillTop = -1;
|
|
3742
|
-
|
|
3789
|
+
scopeStackTop = -1;
|
|
3743
3790
|
let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
|
|
3744
3791
|
if (overlapRefsResiduals === undefined) {
|
|
3745
3792
|
overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
|
|
@@ -4224,13 +4271,15 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4224
4271
|
}
|
|
4225
4272
|
case OP_OR_AND_IN_CONST_2:
|
|
4226
4273
|
{
|
|
4227
|
-
// bytecode layout: ref1Idx, ref2Idx, M, aVal0, setBIdx0,
|
|
4274
|
+
// bytecode layout: ref1Idx, ref2Idx, M, (aVal0, setBIdx0, ref1Op0, ref2Op0),
|
|
4275
|
+
// (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
4228
4276
|
// aVal_j is a literal value; setBIdx_j is a constIdx for the merged setB.
|
|
4277
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
4229
4278
|
const ref1Idx = numAt(bytecode[i++]);
|
|
4230
4279
|
const ref2Idx = numAt(bytecode[i++]);
|
|
4231
4280
|
const n = numAt(bytecode[i++]);
|
|
4232
|
-
const
|
|
4233
|
-
i += n *
|
|
4281
|
+
const quadsStart = i;
|
|
4282
|
+
i += n * 4;
|
|
4234
4283
|
const rawKey1 = refRawKeys[ref1Idx];
|
|
4235
4284
|
const rawKey2 = refRawKeys[ref2Idx];
|
|
4236
4285
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
@@ -4239,6 +4288,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4239
4288
|
const unknown2 = v2 === undefined && !strictSet?.has(rawKey2) && (!optionalSet || optionalSet.has(rawKey2));
|
|
4240
4289
|
if (unknown1 || unknown2) {
|
|
4241
4290
|
// Reconstruct the original complex expression tree
|
|
4291
|
+
// When one ref is known, only include entries where the known ref matches
|
|
4242
4292
|
const branches = [opNames[OP_OR]];
|
|
4243
4293
|
const andOp = opNames[OP_AND];
|
|
4244
4294
|
const eqOp = opNames[OP_EQ];
|
|
@@ -4246,12 +4296,71 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4246
4296
|
const r1 = refKeys[ref1Idx];
|
|
4247
4297
|
const r2 = refKeys[ref2Idx];
|
|
4248
4298
|
for (let j = 0; j < n; j++) {
|
|
4249
|
-
const aVal = literalAt(bytecode[
|
|
4250
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4251
|
-
const
|
|
4252
|
-
|
|
4299
|
+
const aVal = literalAt(bytecode[quadsStart + j * 4]);
|
|
4300
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4301
|
+
const ref1OpByte = numAt(bytecode[quadsStart + j * 4 + 2]);
|
|
4302
|
+
const ref2OpByte = numAt(bytecode[quadsStart + j * 4 + 3]);
|
|
4303
|
+
// Use the original operators for both operands
|
|
4304
|
+
const op1 = ref1OpByte === 1 ? inOp : eqOp;
|
|
4305
|
+
const op2 = ref2OpByte === 1 ? inOp : eqOp;
|
|
4306
|
+
// When reconstructing == with a scalar, use the scalar value
|
|
4307
|
+
// When reconstructing IN with a scalar, wrap it in an array
|
|
4308
|
+
const r1Val = op1 === eqOp ? aVal : [aVal];
|
|
4309
|
+
let r2Val = setB;
|
|
4310
|
+
if (op2 === eqOp && Array.isArray(setB) && setB.length === 1) {
|
|
4311
|
+
r2Val = setB[0];
|
|
4312
|
+
}
|
|
4313
|
+
// Skip entries where the known ref doesn't match
|
|
4314
|
+
if (!unknown1 && v1 !== undefined && v1 !== null) {
|
|
4315
|
+
// ref1 is known — only include matching entries
|
|
4316
|
+
// For ==, check exact match; for IN, check if value is in set
|
|
4317
|
+
let matches = false;
|
|
4318
|
+
if (op1 === eqOp) {
|
|
4319
|
+
matches = v1 === aVal;
|
|
4320
|
+
} else {
|
|
4321
|
+
matches = Array.isArray(v1) ? v1.includes(aVal) : v1 === aVal;
|
|
4322
|
+
}
|
|
4323
|
+
if (!matches) {
|
|
4324
|
+
continue;
|
|
4325
|
+
}
|
|
4326
|
+
}
|
|
4327
|
+
if (!unknown2 && v2 !== undefined && v2 !== null) {
|
|
4328
|
+
// ref2 is known — only include matching entries
|
|
4329
|
+
let matches = false;
|
|
4330
|
+
if (op2 === eqOp) {
|
|
4331
|
+
matches = v2 === r2Val;
|
|
4332
|
+
} else {
|
|
4333
|
+
// setB is always an array here (it's a compiled const)
|
|
4334
|
+
matches = Array.isArray(v2) ? setB.some(item => item === v2) : v2 === r2Val;
|
|
4335
|
+
}
|
|
4336
|
+
if (!matches) {
|
|
4337
|
+
continue;
|
|
4338
|
+
}
|
|
4339
|
+
}
|
|
4340
|
+
// Build the AND branch, omitting known refs that already matched
|
|
4341
|
+
const branchOperands = [];
|
|
4342
|
+
if (unknown1) {
|
|
4343
|
+
branchOperands.push([op1, r1, r1Val]);
|
|
4344
|
+
}
|
|
4345
|
+
if (unknown2) {
|
|
4346
|
+
branchOperands.push([op2, r2, r2Val]);
|
|
4347
|
+
}
|
|
4348
|
+
if (branchOperands.length === 1) {
|
|
4349
|
+
branches.push(branchOperands[0]);
|
|
4350
|
+
} else {
|
|
4351
|
+
branches.push([andOp, ...branchOperands]);
|
|
4352
|
+
}
|
|
4353
|
+
}
|
|
4354
|
+
// Handle edge cases: no matches → false, single match → unwrap OR
|
|
4355
|
+
if (branches.length === 1) {
|
|
4356
|
+
// No entries matched
|
|
4357
|
+
stack[++stackTop] = false;
|
|
4358
|
+
} else if (branches.length === 2) {
|
|
4359
|
+
// Only one entry matched — unwrap the OR
|
|
4360
|
+
stack[++stackTop] = branches[1];
|
|
4361
|
+
} else {
|
|
4362
|
+
stack[++stackTop] = makeResidual(branches);
|
|
4253
4363
|
}
|
|
4254
|
-
stack[++stackTop] = makeResidual(branches);
|
|
4255
4364
|
break;
|
|
4256
4365
|
}
|
|
4257
4366
|
|
|
@@ -4259,8 +4368,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4259
4368
|
let found = false;
|
|
4260
4369
|
if (v1 !== null && v1 !== undefined && v2 !== null && v2 !== undefined) {
|
|
4261
4370
|
for (let j = 0; j < n; j++) {
|
|
4262
|
-
if (bytecode[
|
|
4263
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4371
|
+
if (bytecode[quadsStart + j * 4] === v1) {
|
|
4372
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4264
4373
|
let s = overlapSetCache.get(setB);
|
|
4265
4374
|
if (s === undefined) {
|
|
4266
4375
|
s = new Set(setB);
|
|
@@ -4499,13 +4608,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4499
4608
|
if (!needsReconstruct(top) && slotVal(top) === false) {
|
|
4500
4609
|
i += offset;
|
|
4501
4610
|
}
|
|
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
4611
|
break;
|
|
4510
4612
|
}
|
|
4511
4613
|
case OP_JUMP_IF_TRUE:
|
|
@@ -4515,13 +4617,11 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4515
4617
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4516
4618
|
i += offset;
|
|
4517
4619
|
}
|
|
4518
|
-
// Clear spill buffer when transitioning between short-circuit sequences.
|
|
4519
|
-
if (lastJumpOp !== 42) {
|
|
4520
|
-
spillTop = -1;
|
|
4521
|
-
}
|
|
4522
|
-
lastJumpOp = 42;
|
|
4523
4620
|
break;
|
|
4524
4621
|
}
|
|
4622
|
+
case OP_ENTER_SCOPE:
|
|
4623
|
+
scopeStack[++scopeStackTop] = spillTop;
|
|
4624
|
+
break;
|
|
4525
4625
|
case OP_POP:
|
|
4526
4626
|
{
|
|
4527
4627
|
const popped = stack[stackTop--];
|
|
@@ -4536,15 +4636,19 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4536
4636
|
// AND/OR/NOR markers — collect the current stack top plus any residuals
|
|
4537
4637
|
// that were spilled by OP_POP during the short-circuit sequence, then
|
|
4538
4638
|
// apply the simplification logic.
|
|
4639
|
+
//
|
|
4640
|
+
// Each of these opcodes pops the scope base that was pushed by the
|
|
4641
|
+
// matching OP_ENTER_SCOPE at the start of the short-circuit block. Only
|
|
4642
|
+
// spill entries above that base (indices base+1..spillTop) belong to this
|
|
4643
|
+
// scope; entries at 0..base belong to outer scopes and are preserved by
|
|
4644
|
+
// restoring spillTop = base after draining.
|
|
4539
4645
|
case OP_AND:
|
|
4540
4646
|
{
|
|
4541
4647
|
i++; // consume the operand count byte (unused — we use the spill buffer)
|
|
4542
4648
|
const top = stack[stackTop--];
|
|
4543
|
-
|
|
4544
|
-
//
|
|
4545
|
-
|
|
4546
|
-
if (spillTop < 0) {
|
|
4547
|
-
spillTop = -1;
|
|
4649
|
+
const base = scopeStack[scopeStackTop--];
|
|
4650
|
+
// Fast path: no entries were spilled in this scope.
|
|
4651
|
+
if (spillTop === base) {
|
|
4548
4652
|
if (!needsReconstruct(top)) {
|
|
4549
4653
|
stack[++stackTop] = top;
|
|
4550
4654
|
} else {
|
|
@@ -4553,10 +4657,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4553
4657
|
break;
|
|
4554
4658
|
}
|
|
4555
4659
|
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
4660
|
let andDominated = false;
|
|
4559
|
-
for (let j =
|
|
4661
|
+
for (let j = base + 1; j <= spillTop; j++) {
|
|
4560
4662
|
const v = spillBuf[j];
|
|
4561
4663
|
if (!needsReconstruct(v) && slotVal(v) === false) {
|
|
4562
4664
|
andDominated = true;
|
|
@@ -4566,8 +4668,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4566
4668
|
residuals.push(slotSrc(v));
|
|
4567
4669
|
}
|
|
4568
4670
|
}
|
|
4569
|
-
spillTop =
|
|
4570
|
-
// Include the stack top (last operand result)
|
|
4671
|
+
spillTop = base; // restore outer scope
|
|
4571
4672
|
if (!andDominated) {
|
|
4572
4673
|
if (!needsReconstruct(top) && slotVal(top) === false) {
|
|
4573
4674
|
andDominated = true;
|
|
@@ -4590,10 +4691,9 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4590
4691
|
{
|
|
4591
4692
|
i++; // consume the operand count byte
|
|
4592
4693
|
const top = stack[stackTop--];
|
|
4593
|
-
|
|
4594
|
-
//
|
|
4595
|
-
if (spillTop
|
|
4596
|
-
spillTop = -1;
|
|
4694
|
+
const base = scopeStack[scopeStackTop--];
|
|
4695
|
+
// Fast path: no entries were spilled in this scope.
|
|
4696
|
+
if (spillTop === base) {
|
|
4597
4697
|
if (!needsReconstruct(top)) {
|
|
4598
4698
|
stack[++stackTop] = top;
|
|
4599
4699
|
} else {
|
|
@@ -4603,7 +4703,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4603
4703
|
}
|
|
4604
4704
|
const residuals = [];
|
|
4605
4705
|
let orDominated = false;
|
|
4606
|
-
for (let j =
|
|
4706
|
+
for (let j = base + 1; j <= spillTop; j++) {
|
|
4607
4707
|
const v = spillBuf[j];
|
|
4608
4708
|
if (!needsReconstruct(v) && slotVal(v) === true) {
|
|
4609
4709
|
orDominated = true;
|
|
@@ -4613,7 +4713,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4613
4713
|
residuals.push(slotSrc(v));
|
|
4614
4714
|
}
|
|
4615
4715
|
}
|
|
4616
|
-
spillTop =
|
|
4716
|
+
spillTop = base; // restore outer scope
|
|
4617
4717
|
if (!orDominated) {
|
|
4618
4718
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4619
4719
|
orDominated = true;
|
|
@@ -4641,9 +4741,10 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4641
4741
|
// OP_NOT will pass through unchanged.
|
|
4642
4742
|
i++; // consume the operand count byte
|
|
4643
4743
|
const top = stack[stackTop--];
|
|
4744
|
+
const base = scopeStack[scopeStackTop--];
|
|
4644
4745
|
const residuals = [];
|
|
4645
4746
|
let dominated = false;
|
|
4646
|
-
for (let j =
|
|
4747
|
+
for (let j = base + 1; j <= spillTop; j++) {
|
|
4647
4748
|
const v = spillBuf[j];
|
|
4648
4749
|
if (!needsReconstruct(v) && slotVal(v) === true) {
|
|
4649
4750
|
dominated = true;
|
|
@@ -4653,7 +4754,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4653
4754
|
residuals.push(slotSrc(v));
|
|
4654
4755
|
}
|
|
4655
4756
|
}
|
|
4656
|
-
spillTop =
|
|
4757
|
+
spillTop = base; // restore outer scope
|
|
4657
4758
|
if (!dominated) {
|
|
4658
4759
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4659
4760
|
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.
|
|
@@ -2283,7 +2289,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2283
2289
|
return {
|
|
2284
2290
|
rawRef,
|
|
2285
2291
|
refKey,
|
|
2286
|
-
vals: right
|
|
2292
|
+
vals: right,
|
|
2293
|
+
operator: 'in'
|
|
2287
2294
|
};
|
|
2288
2295
|
}
|
|
2289
2296
|
|
|
@@ -2299,7 +2306,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2299
2306
|
return {
|
|
2300
2307
|
rawRef,
|
|
2301
2308
|
refKey,
|
|
2302
|
-
vals: [right]
|
|
2309
|
+
vals: [right],
|
|
2310
|
+
operator: 'eq'
|
|
2303
2311
|
};
|
|
2304
2312
|
}
|
|
2305
2313
|
return null;
|
|
@@ -2331,6 +2339,10 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2331
2339
|
|
|
2332
2340
|
// Inverted index: each distinct setA value → merged setB values across all branches containing it
|
|
2333
2341
|
const setBValsByAValue = new Map();
|
|
2342
|
+
// Track which operators were used for each setA value (for ref1 operand reconstruction)
|
|
2343
|
+
const ref1OpsByAValue = new Map();
|
|
2344
|
+
// Track which operators were used for each setA value (for ref2 operand reconstruction)
|
|
2345
|
+
const ref2OpsByAValue = new Map();
|
|
2334
2346
|
for (let b = 1; b <= nBranches; b++) {
|
|
2335
2347
|
const branch = arr[b];
|
|
2336
2348
|
if (!Array.isArray(branch)) {
|
|
@@ -2386,6 +2398,20 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2386
2398
|
setBVals = new Set();
|
|
2387
2399
|
setBValsByAValue.set(aVal, setBVals);
|
|
2388
2400
|
}
|
|
2401
|
+
// Track ref1 operator
|
|
2402
|
+
let r1Ops = ref1OpsByAValue.get(aVal);
|
|
2403
|
+
if (r1Ops === undefined) {
|
|
2404
|
+
r1Ops = new Set();
|
|
2405
|
+
ref1OpsByAValue.set(aVal, r1Ops);
|
|
2406
|
+
}
|
|
2407
|
+
r1Ops.add(extA.operator);
|
|
2408
|
+
// Track ref2 operator
|
|
2409
|
+
let r2Ops = ref2OpsByAValue.get(aVal);
|
|
2410
|
+
if (r2Ops === undefined) {
|
|
2411
|
+
r2Ops = new Set();
|
|
2412
|
+
ref2OpsByAValue.set(aVal, r2Ops);
|
|
2413
|
+
}
|
|
2414
|
+
r2Ops.add(extB.operator);
|
|
2389
2415
|
for (const bVal of extB.vals) {
|
|
2390
2416
|
setBVals.add(bVal);
|
|
2391
2417
|
}
|
|
@@ -2397,14 +2423,25 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2397
2423
|
|
|
2398
2424
|
// Build entries: one (literal aVal, mergedSetBIdx) per distinct setA value
|
|
2399
2425
|
const entries = [];
|
|
2426
|
+
// Track operators per entry: [ref1Op, ref2Op]
|
|
2427
|
+
const entryOperators = [];
|
|
2400
2428
|
for (const [aVal, setBVals] of setBValsByAValue) {
|
|
2401
2429
|
const mergedSetB = [...setBVals].filter(v => v !== undefined);
|
|
2402
|
-
|
|
2430
|
+
const constIdx = internConst(mergedSetB, state);
|
|
2431
|
+
entries.push([aVal, constIdx]);
|
|
2432
|
+
// Determine ref1 operator: if all branches used 'eq', preserve 'eq'; otherwise use 'in'
|
|
2433
|
+
const r1Ops = ref1OpsByAValue.get(aVal);
|
|
2434
|
+
const ref1Op = r1Ops !== undefined && r1Ops.size === 1 && r1Ops.has('eq') ? 'eq' : 'in';
|
|
2435
|
+
// Determine ref2 operator: if all branches used 'eq', preserve 'eq'; otherwise use 'in'
|
|
2436
|
+
const r2Ops = ref2OpsByAValue.get(aVal);
|
|
2437
|
+
const ref2Op = r2Ops !== undefined && r2Ops.size === 1 && r2Ops.has('eq') ? 'eq' : 'in';
|
|
2438
|
+
entryOperators.push([ref1Op, ref2Op]);
|
|
2403
2439
|
}
|
|
2404
2440
|
return {
|
|
2405
2441
|
ref1Raw,
|
|
2406
2442
|
ref2Raw,
|
|
2407
|
-
entries
|
|
2443
|
+
entries,
|
|
2444
|
+
entryOperators
|
|
2408
2445
|
};
|
|
2409
2446
|
}
|
|
2410
2447
|
|
|
@@ -2415,6 +2452,7 @@ function emitShortCircuit(arr, jumpOp, markerOp, state) {
|
|
|
2415
2452
|
} = state;
|
|
2416
2453
|
const jumpSlots = [];
|
|
2417
2454
|
const last = arr.length - 1;
|
|
2455
|
+
bytecode.push(OP_ENTER_SCOPE);
|
|
2418
2456
|
for (let i = 1; i < last; i++) {
|
|
2419
2457
|
emitExpression(arr[i], state);
|
|
2420
2458
|
bytecode.push(jumpOp);
|
|
@@ -2461,18 +2499,22 @@ function emitExpression(raw, state) {
|
|
|
2461
2499
|
const {
|
|
2462
2500
|
ref1Raw,
|
|
2463
2501
|
ref2Raw,
|
|
2464
|
-
entries
|
|
2502
|
+
entries,
|
|
2503
|
+
entryOperators
|
|
2465
2504
|
} = orAnd2;
|
|
2466
2505
|
const {
|
|
2467
2506
|
bytecode
|
|
2468
2507
|
} = state;
|
|
2469
2508
|
const ref1Idx = internRef(ref1Raw, state);
|
|
2470
2509
|
const ref2Idx = internRef(ref2Raw, state);
|
|
2471
|
-
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M
|
|
2510
|
+
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M (aVal0 setBIdx0 ref1Op0 ref2Op0) ...
|
|
2472
2511
|
// M is the number of distinct setA values across all branches (after inverted-index merge).
|
|
2512
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
2473
2513
|
bytecode.push(OP_OR_AND_IN_CONST_2, ref1Idx, ref2Idx, entries.length);
|
|
2474
|
-
for (
|
|
2475
|
-
|
|
2514
|
+
for (let j = 0; j < entries.length; j++) {
|
|
2515
|
+
const [aVal, mergedSetBIdx] = entries[j];
|
|
2516
|
+
const [ref1Op, ref2Op] = entryOperators[j];
|
|
2517
|
+
bytecode.push(aVal, mergedSetBIdx, ref1Op === 'in' ? 1 : 0, ref2Op === 'in' ? 1 : 0);
|
|
2476
2518
|
}
|
|
2477
2519
|
return;
|
|
2478
2520
|
}
|
|
@@ -3204,20 +3246,21 @@ function interpret(compiled, ctx) {
|
|
|
3204
3246
|
}
|
|
3205
3247
|
case OP_OR_AND_IN_CONST_2:
|
|
3206
3248
|
{
|
|
3207
|
-
// bytecode layout: ref1Idx, ref2Idx, M, v0, setBIdx0, v1, setBIdx1,
|
|
3249
|
+
// bytecode layout: ref1Idx, ref2Idx, M, (v0, setBIdx0, ref1Op0, ref2Op0), (v1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
3250
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in' (unused at runtime, kept for simplifier)
|
|
3208
3251
|
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3209
3252
|
const ref1Idx = numAt$1(bytecode[++i]);
|
|
3210
3253
|
const ref2Idx = numAt$1(bytecode[++i]);
|
|
3211
3254
|
const m = numAt$1(bytecode[++i]);
|
|
3212
3255
|
const entriesStart = i + 1;
|
|
3213
|
-
i += m *
|
|
3256
|
+
i += m * 4;
|
|
3214
3257
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
3215
3258
|
const v2 = resolveCompactRef(refs[ref2Idx], ctx);
|
|
3216
3259
|
let found = false;
|
|
3217
3260
|
if (v1 !== undefined && v1 !== null && v2 !== undefined && v2 !== null) {
|
|
3218
3261
|
for (let j = 0; j < m; j++) {
|
|
3219
|
-
if (bytecode[entriesStart + j *
|
|
3220
|
-
found = constSets[numAt$1(bytecode[entriesStart + j *
|
|
3262
|
+
if (bytecode[entriesStart + j * 4] === v1) {
|
|
3263
|
+
found = constSets[numAt$1(bytecode[entriesStart + j * 4 + 1])].has(v2);
|
|
3221
3264
|
break;
|
|
3222
3265
|
}
|
|
3223
3266
|
}
|
|
@@ -3389,6 +3432,8 @@ function interpret(compiled, ctx) {
|
|
|
3389
3432
|
case OP_POP:
|
|
3390
3433
|
stackTop$1--;
|
|
3391
3434
|
break;
|
|
3435
|
+
case OP_ENTER_SCOPE:
|
|
3436
|
+
break;
|
|
3392
3437
|
case OP_AND:
|
|
3393
3438
|
case OP_OR:
|
|
3394
3439
|
case OP_NOR:
|
|
@@ -3675,10 +3720,12 @@ let resolvedRefUsedCount = 0;
|
|
|
3675
3720
|
// can include it in the reconstructed expression.
|
|
3676
3721
|
const spillBuf = new Array(MAX_STACK);
|
|
3677
3722
|
let spillTop = -1;
|
|
3678
|
-
//
|
|
3679
|
-
//
|
|
3680
|
-
//
|
|
3681
|
-
|
|
3723
|
+
// Scope stack for the spill buffer: each OP_ENTER_SCOPE pushes the current
|
|
3724
|
+
// spillTop so that OP_AND/OR/NOR can drain only the entries added within their
|
|
3725
|
+
// own scope (base+1..spillTop) and restore the outer scope (spillTop = base).
|
|
3726
|
+
// This prevents nested AND/OR from stealing residuals accumulated by outer scopes.
|
|
3727
|
+
const scopeStack = new Array(MAX_STACK);
|
|
3728
|
+
let scopeStackTop = -1;
|
|
3682
3729
|
function relationalCompare(left, right, op) {
|
|
3683
3730
|
if (isNumber(left) && isNumber(right)) {
|
|
3684
3731
|
if (op === OP_GT) {
|
|
@@ -3735,7 +3782,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
3735
3782
|
} = compiled;
|
|
3736
3783
|
stackTop = -1;
|
|
3737
3784
|
spillTop = -1;
|
|
3738
|
-
|
|
3785
|
+
scopeStackTop = -1;
|
|
3739
3786
|
let overlapRefsResiduals = overlapRefsResidualsCache.get(compiled);
|
|
3740
3787
|
if (overlapRefsResiduals === undefined) {
|
|
3741
3788
|
overlapRefsResiduals = new Map(compiled.overlapRefsResiduals);
|
|
@@ -4220,13 +4267,15 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4220
4267
|
}
|
|
4221
4268
|
case OP_OR_AND_IN_CONST_2:
|
|
4222
4269
|
{
|
|
4223
|
-
// bytecode layout: ref1Idx, ref2Idx, M, aVal0, setBIdx0,
|
|
4270
|
+
// bytecode layout: ref1Idx, ref2Idx, M, (aVal0, setBIdx0, ref1Op0, ref2Op0),
|
|
4271
|
+
// (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
4224
4272
|
// aVal_j is a literal value; setBIdx_j is a constIdx for the merged setB.
|
|
4273
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
4225
4274
|
const ref1Idx = numAt(bytecode[i++]);
|
|
4226
4275
|
const ref2Idx = numAt(bytecode[i++]);
|
|
4227
4276
|
const n = numAt(bytecode[i++]);
|
|
4228
|
-
const
|
|
4229
|
-
i += n *
|
|
4277
|
+
const quadsStart = i;
|
|
4278
|
+
i += n * 4;
|
|
4230
4279
|
const rawKey1 = refRawKeys[ref1Idx];
|
|
4231
4280
|
const rawKey2 = refRawKeys[ref2Idx];
|
|
4232
4281
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
@@ -4235,6 +4284,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4235
4284
|
const unknown2 = v2 === undefined && !strictSet?.has(rawKey2) && (!optionalSet || optionalSet.has(rawKey2));
|
|
4236
4285
|
if (unknown1 || unknown2) {
|
|
4237
4286
|
// Reconstruct the original complex expression tree
|
|
4287
|
+
// When one ref is known, only include entries where the known ref matches
|
|
4238
4288
|
const branches = [opNames[OP_OR]];
|
|
4239
4289
|
const andOp = opNames[OP_AND];
|
|
4240
4290
|
const eqOp = opNames[OP_EQ];
|
|
@@ -4242,12 +4292,71 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4242
4292
|
const r1 = refKeys[ref1Idx];
|
|
4243
4293
|
const r2 = refKeys[ref2Idx];
|
|
4244
4294
|
for (let j = 0; j < n; j++) {
|
|
4245
|
-
const aVal = literalAt(bytecode[
|
|
4246
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4247
|
-
const
|
|
4248
|
-
|
|
4295
|
+
const aVal = literalAt(bytecode[quadsStart + j * 4]);
|
|
4296
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4297
|
+
const ref1OpByte = numAt(bytecode[quadsStart + j * 4 + 2]);
|
|
4298
|
+
const ref2OpByte = numAt(bytecode[quadsStart + j * 4 + 3]);
|
|
4299
|
+
// Use the original operators for both operands
|
|
4300
|
+
const op1 = ref1OpByte === 1 ? inOp : eqOp;
|
|
4301
|
+
const op2 = ref2OpByte === 1 ? inOp : eqOp;
|
|
4302
|
+
// When reconstructing == with a scalar, use the scalar value
|
|
4303
|
+
// When reconstructing IN with a scalar, wrap it in an array
|
|
4304
|
+
const r1Val = op1 === eqOp ? aVal : [aVal];
|
|
4305
|
+
let r2Val = setB;
|
|
4306
|
+
if (op2 === eqOp && Array.isArray(setB) && setB.length === 1) {
|
|
4307
|
+
r2Val = setB[0];
|
|
4308
|
+
}
|
|
4309
|
+
// Skip entries where the known ref doesn't match
|
|
4310
|
+
if (!unknown1 && v1 !== undefined && v1 !== null) {
|
|
4311
|
+
// ref1 is known — only include matching entries
|
|
4312
|
+
// For ==, check exact match; for IN, check if value is in set
|
|
4313
|
+
let matches = false;
|
|
4314
|
+
if (op1 === eqOp) {
|
|
4315
|
+
matches = v1 === aVal;
|
|
4316
|
+
} else {
|
|
4317
|
+
matches = Array.isArray(v1) ? v1.includes(aVal) : v1 === aVal;
|
|
4318
|
+
}
|
|
4319
|
+
if (!matches) {
|
|
4320
|
+
continue;
|
|
4321
|
+
}
|
|
4322
|
+
}
|
|
4323
|
+
if (!unknown2 && v2 !== undefined && v2 !== null) {
|
|
4324
|
+
// ref2 is known — only include matching entries
|
|
4325
|
+
let matches = false;
|
|
4326
|
+
if (op2 === eqOp) {
|
|
4327
|
+
matches = v2 === r2Val;
|
|
4328
|
+
} else {
|
|
4329
|
+
// setB is always an array here (it's a compiled const)
|
|
4330
|
+
matches = Array.isArray(v2) ? setB.some(item => item === v2) : v2 === r2Val;
|
|
4331
|
+
}
|
|
4332
|
+
if (!matches) {
|
|
4333
|
+
continue;
|
|
4334
|
+
}
|
|
4335
|
+
}
|
|
4336
|
+
// Build the AND branch, omitting known refs that already matched
|
|
4337
|
+
const branchOperands = [];
|
|
4338
|
+
if (unknown1) {
|
|
4339
|
+
branchOperands.push([op1, r1, r1Val]);
|
|
4340
|
+
}
|
|
4341
|
+
if (unknown2) {
|
|
4342
|
+
branchOperands.push([op2, r2, r2Val]);
|
|
4343
|
+
}
|
|
4344
|
+
if (branchOperands.length === 1) {
|
|
4345
|
+
branches.push(branchOperands[0]);
|
|
4346
|
+
} else {
|
|
4347
|
+
branches.push([andOp, ...branchOperands]);
|
|
4348
|
+
}
|
|
4349
|
+
}
|
|
4350
|
+
// Handle edge cases: no matches → false, single match → unwrap OR
|
|
4351
|
+
if (branches.length === 1) {
|
|
4352
|
+
// No entries matched
|
|
4353
|
+
stack[++stackTop] = false;
|
|
4354
|
+
} else if (branches.length === 2) {
|
|
4355
|
+
// Only one entry matched — unwrap the OR
|
|
4356
|
+
stack[++stackTop] = branches[1];
|
|
4357
|
+
} else {
|
|
4358
|
+
stack[++stackTop] = makeResidual(branches);
|
|
4249
4359
|
}
|
|
4250
|
-
stack[++stackTop] = makeResidual(branches);
|
|
4251
4360
|
break;
|
|
4252
4361
|
}
|
|
4253
4362
|
|
|
@@ -4255,8 +4364,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4255
4364
|
let found = false;
|
|
4256
4365
|
if (v1 !== null && v1 !== undefined && v2 !== null && v2 !== undefined) {
|
|
4257
4366
|
for (let j = 0; j < n; j++) {
|
|
4258
|
-
if (bytecode[
|
|
4259
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4367
|
+
if (bytecode[quadsStart + j * 4] === v1) {
|
|
4368
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4260
4369
|
let s = overlapSetCache.get(setB);
|
|
4261
4370
|
if (s === undefined) {
|
|
4262
4371
|
s = new Set(setB);
|
|
@@ -4495,13 +4604,6 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4495
4604
|
if (!needsReconstruct(top) && slotVal(top) === false) {
|
|
4496
4605
|
i += offset;
|
|
4497
4606
|
}
|
|
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
4607
|
break;
|
|
4506
4608
|
}
|
|
4507
4609
|
case OP_JUMP_IF_TRUE:
|
|
@@ -4511,13 +4613,11 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4511
4613
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4512
4614
|
i += offset;
|
|
4513
4615
|
}
|
|
4514
|
-
// Clear spill buffer when transitioning between short-circuit sequences.
|
|
4515
|
-
if (lastJumpOp !== 42) {
|
|
4516
|
-
spillTop = -1;
|
|
4517
|
-
}
|
|
4518
|
-
lastJumpOp = 42;
|
|
4519
4616
|
break;
|
|
4520
4617
|
}
|
|
4618
|
+
case OP_ENTER_SCOPE:
|
|
4619
|
+
scopeStack[++scopeStackTop] = spillTop;
|
|
4620
|
+
break;
|
|
4521
4621
|
case OP_POP:
|
|
4522
4622
|
{
|
|
4523
4623
|
const popped = stack[stackTop--];
|
|
@@ -4532,15 +4632,19 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4532
4632
|
// AND/OR/NOR markers — collect the current stack top plus any residuals
|
|
4533
4633
|
// that were spilled by OP_POP during the short-circuit sequence, then
|
|
4534
4634
|
// apply the simplification logic.
|
|
4635
|
+
//
|
|
4636
|
+
// Each of these opcodes pops the scope base that was pushed by the
|
|
4637
|
+
// matching OP_ENTER_SCOPE at the start of the short-circuit block. Only
|
|
4638
|
+
// spill entries above that base (indices base+1..spillTop) belong to this
|
|
4639
|
+
// scope; entries at 0..base belong to outer scopes and are preserved by
|
|
4640
|
+
// restoring spillTop = base after draining.
|
|
4535
4641
|
case OP_AND:
|
|
4536
4642
|
{
|
|
4537
4643
|
i++; // consume the operand count byte (unused — we use the spill buffer)
|
|
4538
4644
|
const top = stack[stackTop--];
|
|
4539
|
-
|
|
4540
|
-
//
|
|
4541
|
-
|
|
4542
|
-
if (spillTop < 0) {
|
|
4543
|
-
spillTop = -1;
|
|
4645
|
+
const base = scopeStack[scopeStackTop--];
|
|
4646
|
+
// Fast path: no entries were spilled in this scope.
|
|
4647
|
+
if (spillTop === base) {
|
|
4544
4648
|
if (!needsReconstruct(top)) {
|
|
4545
4649
|
stack[++stackTop] = top;
|
|
4546
4650
|
} else {
|
|
@@ -4549,10 +4653,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4549
4653
|
break;
|
|
4550
4654
|
}
|
|
4551
4655
|
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
4656
|
let andDominated = false;
|
|
4555
|
-
for (let j =
|
|
4657
|
+
for (let j = base + 1; j <= spillTop; j++) {
|
|
4556
4658
|
const v = spillBuf[j];
|
|
4557
4659
|
if (!needsReconstruct(v) && slotVal(v) === false) {
|
|
4558
4660
|
andDominated = true;
|
|
@@ -4562,8 +4664,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4562
4664
|
residuals.push(slotSrc(v));
|
|
4563
4665
|
}
|
|
4564
4666
|
}
|
|
4565
|
-
spillTop =
|
|
4566
|
-
// Include the stack top (last operand result)
|
|
4667
|
+
spillTop = base; // restore outer scope
|
|
4567
4668
|
if (!andDominated) {
|
|
4568
4669
|
if (!needsReconstruct(top) && slotVal(top) === false) {
|
|
4569
4670
|
andDominated = true;
|
|
@@ -4586,10 +4687,9 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4586
4687
|
{
|
|
4587
4688
|
i++; // consume the operand count byte
|
|
4588
4689
|
const top = stack[stackTop--];
|
|
4589
|
-
|
|
4590
|
-
//
|
|
4591
|
-
if (spillTop
|
|
4592
|
-
spillTop = -1;
|
|
4690
|
+
const base = scopeStack[scopeStackTop--];
|
|
4691
|
+
// Fast path: no entries were spilled in this scope.
|
|
4692
|
+
if (spillTop === base) {
|
|
4593
4693
|
if (!needsReconstruct(top)) {
|
|
4594
4694
|
stack[++stackTop] = top;
|
|
4595
4695
|
} else {
|
|
@@ -4599,7 +4699,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4599
4699
|
}
|
|
4600
4700
|
const residuals = [];
|
|
4601
4701
|
let orDominated = false;
|
|
4602
|
-
for (let j =
|
|
4702
|
+
for (let j = base + 1; j <= spillTop; j++) {
|
|
4603
4703
|
const v = spillBuf[j];
|
|
4604
4704
|
if (!needsReconstruct(v) && slotVal(v) === true) {
|
|
4605
4705
|
orDominated = true;
|
|
@@ -4609,7 +4709,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4609
4709
|
residuals.push(slotSrc(v));
|
|
4610
4710
|
}
|
|
4611
4711
|
}
|
|
4612
|
-
spillTop =
|
|
4712
|
+
spillTop = base; // restore outer scope
|
|
4613
4713
|
if (!orDominated) {
|
|
4614
4714
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4615
4715
|
orDominated = true;
|
|
@@ -4637,9 +4737,10 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4637
4737
|
// OP_NOT will pass through unchanged.
|
|
4638
4738
|
i++; // consume the operand count byte
|
|
4639
4739
|
const top = stack[stackTop--];
|
|
4740
|
+
const base = scopeStack[scopeStackTop--];
|
|
4640
4741
|
const residuals = [];
|
|
4641
4742
|
let dominated = false;
|
|
4642
|
-
for (let j =
|
|
4743
|
+
for (let j = base + 1; j <= spillTop; j++) {
|
|
4643
4744
|
const v = spillBuf[j];
|
|
4644
4745
|
if (!needsReconstruct(v) && slotVal(v) === true) {
|
|
4645
4746
|
dominated = true;
|
|
@@ -4649,7 +4750,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4649
4750
|
residuals.push(slotSrc(v));
|
|
4650
4751
|
}
|
|
4651
4752
|
}
|
|
4652
|
-
spillTop =
|
|
4753
|
+
spillTop = base; // restore outer scope
|
|
4653
4754
|
if (!dominated) {
|
|
4654
4755
|
if (!needsReconstruct(top) && slotVal(top) === true) {
|
|
4655
4756
|
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.3",
|
|
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
|
@@ -8,20 +8,18 @@
|
|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
[](https://github.com/briza-insurance/illogical/actions?branch=master)
|
|
17
|
+
[](https://scorecard.dev/viewer/?uri=github.com/briza-insurance/illogical)
|
|
18
|
+
[](https://badge.fury.io/js/@briza%2Fillogical)
|
|
19
|
+
[](https://packagephobia.com/result?p=@briza/illogical)
|
|
20
|
+

|
|
21
|
+

|
|
22
|
+
|
|
25
23
|
</div>
|
|
26
24
|
|
|
27
25
|
---
|