@briza/illogical 2.2.2 → 2.2.4
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 -29
- package/lib/illogical.esm.js +158 -29
- package/package.json +8 -6
- package/readme.md +22 -9
- package/types/bytecode/compiler.d.ts +59 -1
- package/types/common/evaluable.d.ts +1 -2
- package/types/index.d.ts +6 -0
package/lib/illogical.cjs
CHANGED
|
@@ -1304,7 +1304,7 @@ class Present extends Comparison {
|
|
|
1304
1304
|
}
|
|
1305
1305
|
|
|
1306
1306
|
// Operator key
|
|
1307
|
-
const OPERATOR$6 = Symbol('
|
|
1307
|
+
const OPERATOR$6 = Symbol('SUFFIX');
|
|
1308
1308
|
|
|
1309
1309
|
/**
|
|
1310
1310
|
* Suffix comparison expression
|
|
@@ -2293,7 +2293,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2293
2293
|
return {
|
|
2294
2294
|
rawRef,
|
|
2295
2295
|
refKey,
|
|
2296
|
-
vals: right
|
|
2296
|
+
vals: right,
|
|
2297
|
+
operator: 'in'
|
|
2297
2298
|
};
|
|
2298
2299
|
}
|
|
2299
2300
|
|
|
@@ -2309,7 +2310,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2309
2310
|
return {
|
|
2310
2311
|
rawRef,
|
|
2311
2312
|
refKey,
|
|
2312
|
-
vals: [right]
|
|
2313
|
+
vals: [right],
|
|
2314
|
+
operator: 'eq'
|
|
2313
2315
|
};
|
|
2314
2316
|
}
|
|
2315
2317
|
return null;
|
|
@@ -2341,6 +2343,10 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2341
2343
|
|
|
2342
2344
|
// Inverted index: each distinct setA value → merged setB values across all branches containing it
|
|
2343
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();
|
|
2344
2350
|
for (let b = 1; b <= nBranches; b++) {
|
|
2345
2351
|
const branch = arr[b];
|
|
2346
2352
|
if (!Array.isArray(branch)) {
|
|
@@ -2396,6 +2402,20 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2396
2402
|
setBVals = new Set();
|
|
2397
2403
|
setBValsByAValue.set(aVal, setBVals);
|
|
2398
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);
|
|
2399
2419
|
for (const bVal of extB.vals) {
|
|
2400
2420
|
setBVals.add(bVal);
|
|
2401
2421
|
}
|
|
@@ -2407,14 +2427,25 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2407
2427
|
|
|
2408
2428
|
// Build entries: one (literal aVal, mergedSetBIdx) per distinct setA value
|
|
2409
2429
|
const entries = [];
|
|
2430
|
+
// Track operators per entry: [ref1Op, ref2Op]
|
|
2431
|
+
const entryOperators = [];
|
|
2410
2432
|
for (const [aVal, setBVals] of setBValsByAValue) {
|
|
2411
2433
|
const mergedSetB = [...setBVals].filter(v => v !== undefined);
|
|
2412
|
-
|
|
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]);
|
|
2413
2443
|
}
|
|
2414
2444
|
return {
|
|
2415
2445
|
ref1Raw,
|
|
2416
2446
|
ref2Raw,
|
|
2417
|
-
entries
|
|
2447
|
+
entries,
|
|
2448
|
+
entryOperators
|
|
2418
2449
|
};
|
|
2419
2450
|
}
|
|
2420
2451
|
|
|
@@ -2467,23 +2498,29 @@ function emitExpression(raw, state) {
|
|
|
2467
2498
|
return;
|
|
2468
2499
|
}
|
|
2469
2500
|
if (operator === maps.orOp) {
|
|
2470
|
-
|
|
2501
|
+
// Skip the merge when compiling for simplify so the nested structure can
|
|
2502
|
+
// be reconstructed verbatim (the merge collapses branches by ref1 value).
|
|
2503
|
+
const orAnd2 = state.simplify ? null : detectOrAndIn2Pattern(arr, state);
|
|
2471
2504
|
if (orAnd2 !== null) {
|
|
2472
2505
|
const {
|
|
2473
2506
|
ref1Raw,
|
|
2474
2507
|
ref2Raw,
|
|
2475
|
-
entries
|
|
2508
|
+
entries,
|
|
2509
|
+
entryOperators
|
|
2476
2510
|
} = orAnd2;
|
|
2477
2511
|
const {
|
|
2478
2512
|
bytecode
|
|
2479
2513
|
} = state;
|
|
2480
2514
|
const ref1Idx = internRef(ref1Raw, state);
|
|
2481
2515
|
const ref2Idx = internRef(ref2Raw, state);
|
|
2482
|
-
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M
|
|
2516
|
+
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M (aVal0 setBIdx0 ref1Op0 ref2Op0) ...
|
|
2483
2517
|
// M is the number of distinct setA values across all branches (after inverted-index merge).
|
|
2518
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
2484
2519
|
bytecode.push(OP_OR_AND_IN_CONST_2, ref1Idx, ref2Idx, entries.length);
|
|
2485
|
-
for (
|
|
2486
|
-
|
|
2520
|
+
for (let j = 0; j < entries.length; j++) {
|
|
2521
|
+
const [aVal, mergedSetBIdx] = entries[j];
|
|
2522
|
+
const [ref1Op, ref2Op] = entryOperators[j];
|
|
2523
|
+
bytecode.push(aVal, mergedSetBIdx, ref1Op === 'in' ? 1 : 0, ref2Op === 'in' ? 1 : 0);
|
|
2487
2524
|
}
|
|
2488
2525
|
return;
|
|
2489
2526
|
}
|
|
@@ -2740,7 +2777,7 @@ function emitExpression(raw, state) {
|
|
|
2740
2777
|
* Compile a raw ExpressionInput into bytecode.
|
|
2741
2778
|
* The result should be cached and reused across evaluate() calls.
|
|
2742
2779
|
*/
|
|
2743
|
-
function compile(raw, opts) {
|
|
2780
|
+
function compile(raw, opts, simplify = false) {
|
|
2744
2781
|
const maps = buildOperatorMaps(opts);
|
|
2745
2782
|
const state = {
|
|
2746
2783
|
bytecode: [],
|
|
@@ -2755,7 +2792,8 @@ function compile(raw, opts) {
|
|
|
2755
2792
|
consts: [],
|
|
2756
2793
|
constIndex: new Map(),
|
|
2757
2794
|
overlapRefsEntries: [],
|
|
2758
|
-
directionEntries: []
|
|
2795
|
+
directionEntries: [],
|
|
2796
|
+
simplify
|
|
2759
2797
|
};
|
|
2760
2798
|
emitExpression(raw, state);
|
|
2761
2799
|
|
|
@@ -3215,20 +3253,22 @@ function interpret(compiled, ctx) {
|
|
|
3215
3253
|
}
|
|
3216
3254
|
case OP_OR_AND_IN_CONST_2:
|
|
3217
3255
|
{
|
|
3218
|
-
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3256
|
+
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3257
|
+
// (aVal0, setBIdx0, ref1Op0, ref2Op0), (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
3258
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in' (unused at runtime, kept for simplifier)
|
|
3219
3259
|
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3220
3260
|
const ref1Idx = numAt$1(bytecode[++i]);
|
|
3221
3261
|
const ref2Idx = numAt$1(bytecode[++i]);
|
|
3222
3262
|
const m = numAt$1(bytecode[++i]);
|
|
3223
3263
|
const entriesStart = i + 1;
|
|
3224
|
-
i += m *
|
|
3264
|
+
i += m * 4;
|
|
3225
3265
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
3226
3266
|
const v2 = resolveCompactRef(refs[ref2Idx], ctx);
|
|
3227
3267
|
let found = false;
|
|
3228
3268
|
if (v1 !== undefined && v1 !== null && v2 !== undefined && v2 !== null) {
|
|
3229
3269
|
for (let j = 0; j < m; j++) {
|
|
3230
|
-
if (bytecode[entriesStart + j *
|
|
3231
|
-
found = constSets[numAt$1(bytecode[entriesStart + j *
|
|
3270
|
+
if (bytecode[entriesStart + j * 4] === v1) {
|
|
3271
|
+
found = constSets[numAt$1(bytecode[entriesStart + j * 4 + 1])].has(v2);
|
|
3232
3272
|
break;
|
|
3233
3273
|
}
|
|
3234
3274
|
}
|
|
@@ -3333,8 +3373,14 @@ function interpret(compiled, ctx) {
|
|
|
3333
3373
|
break;
|
|
3334
3374
|
}
|
|
3335
3375
|
const values = new Array(n);
|
|
3376
|
+
// Fixed destination slot for the single result value: the bottom-most
|
|
3377
|
+
// operand slot. The operands occupy [stackTop - n + 1 .. stackTop].
|
|
3378
|
+
// Writing here (instead of to stack[++stackTop]) keeps the result in a
|
|
3379
|
+
// stable slot even when the loop below breaks early on a missing
|
|
3380
|
+
// operand, so the next opcode never reads a stale operand value.
|
|
3381
|
+
const resultSlot = stackTop$1 - n + 1;
|
|
3336
3382
|
let hasNull = false;
|
|
3337
|
-
const isDateArithmetic = !isNaN(toDateNumber(stack$1[
|
|
3383
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[resultSlot])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3338
3384
|
for (let j = n - 1; j >= 0; j--) {
|
|
3339
3385
|
const v = stack$1[stackTop$1--];
|
|
3340
3386
|
if (v === null || v === undefined) {
|
|
@@ -3355,7 +3401,9 @@ function interpret(compiled, ctx) {
|
|
|
3355
3401
|
values[j] = v;
|
|
3356
3402
|
}
|
|
3357
3403
|
}
|
|
3358
|
-
|
|
3404
|
+
const result = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce$1(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce$1(values, op) : false;
|
|
3405
|
+
stack$1[resultSlot] = result;
|
|
3406
|
+
stackTop$1 = resultSlot;
|
|
3359
3407
|
break;
|
|
3360
3408
|
}
|
|
3361
3409
|
|
|
@@ -4235,13 +4283,15 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4235
4283
|
}
|
|
4236
4284
|
case OP_OR_AND_IN_CONST_2:
|
|
4237
4285
|
{
|
|
4238
|
-
// bytecode layout: ref1Idx, ref2Idx, M, aVal0, setBIdx0,
|
|
4286
|
+
// bytecode layout: ref1Idx, ref2Idx, M, (aVal0, setBIdx0, ref1Op0, ref2Op0),
|
|
4287
|
+
// (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
4239
4288
|
// aVal_j is a literal value; setBIdx_j is a constIdx for the merged setB.
|
|
4289
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
4240
4290
|
const ref1Idx = numAt(bytecode[i++]);
|
|
4241
4291
|
const ref2Idx = numAt(bytecode[i++]);
|
|
4242
4292
|
const n = numAt(bytecode[i++]);
|
|
4243
|
-
const
|
|
4244
|
-
i += n *
|
|
4293
|
+
const quadsStart = i;
|
|
4294
|
+
i += n * 4;
|
|
4245
4295
|
const rawKey1 = refRawKeys[ref1Idx];
|
|
4246
4296
|
const rawKey2 = refRawKeys[ref2Idx];
|
|
4247
4297
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
@@ -4250,6 +4300,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4250
4300
|
const unknown2 = v2 === undefined && !strictSet?.has(rawKey2) && (!optionalSet || optionalSet.has(rawKey2));
|
|
4251
4301
|
if (unknown1 || unknown2) {
|
|
4252
4302
|
// Reconstruct the original complex expression tree
|
|
4303
|
+
// When one ref is known, only include entries where the known ref matches
|
|
4253
4304
|
const branches = [opNames[OP_OR]];
|
|
4254
4305
|
const andOp = opNames[OP_AND];
|
|
4255
4306
|
const eqOp = opNames[OP_EQ];
|
|
@@ -4257,12 +4308,71 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4257
4308
|
const r1 = refKeys[ref1Idx];
|
|
4258
4309
|
const r2 = refKeys[ref2Idx];
|
|
4259
4310
|
for (let j = 0; j < n; j++) {
|
|
4260
|
-
const aVal = literalAt(bytecode[
|
|
4261
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4262
|
-
const
|
|
4263
|
-
|
|
4311
|
+
const aVal = literalAt(bytecode[quadsStart + j * 4]);
|
|
4312
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4313
|
+
const ref1OpByte = numAt(bytecode[quadsStart + j * 4 + 2]);
|
|
4314
|
+
const ref2OpByte = numAt(bytecode[quadsStart + j * 4 + 3]);
|
|
4315
|
+
// Use the original operators for both operands
|
|
4316
|
+
const op1 = ref1OpByte === 1 ? inOp : eqOp;
|
|
4317
|
+
const op2 = ref2OpByte === 1 ? inOp : eqOp;
|
|
4318
|
+
// When reconstructing == with a scalar, use the scalar value
|
|
4319
|
+
// When reconstructing IN with a scalar, wrap it in an array
|
|
4320
|
+
const r1Val = op1 === eqOp ? aVal : [aVal];
|
|
4321
|
+
let r2Val = setB;
|
|
4322
|
+
if (op2 === eqOp && Array.isArray(setB) && setB.length === 1) {
|
|
4323
|
+
r2Val = setB[0];
|
|
4324
|
+
}
|
|
4325
|
+
// Skip entries where the known ref doesn't match
|
|
4326
|
+
if (!unknown1 && v1 !== undefined && v1 !== null) {
|
|
4327
|
+
// ref1 is known — only include matching entries
|
|
4328
|
+
// For ==, check exact match; for IN, check if value is in set
|
|
4329
|
+
let matches = false;
|
|
4330
|
+
if (op1 === eqOp) {
|
|
4331
|
+
matches = v1 === aVal;
|
|
4332
|
+
} else {
|
|
4333
|
+
matches = Array.isArray(v1) ? v1.includes(aVal) : v1 === aVal;
|
|
4334
|
+
}
|
|
4335
|
+
if (!matches) {
|
|
4336
|
+
continue;
|
|
4337
|
+
}
|
|
4338
|
+
}
|
|
4339
|
+
if (!unknown2 && v2 !== undefined && v2 !== null) {
|
|
4340
|
+
// ref2 is known — only include matching entries
|
|
4341
|
+
let matches = false;
|
|
4342
|
+
if (op2 === eqOp) {
|
|
4343
|
+
matches = v2 === r2Val;
|
|
4344
|
+
} else {
|
|
4345
|
+
// setB is always an array here (it's a compiled const)
|
|
4346
|
+
matches = Array.isArray(v2) ? setB.some(item => item === v2) : v2 === r2Val;
|
|
4347
|
+
}
|
|
4348
|
+
if (!matches) {
|
|
4349
|
+
continue;
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
// Build the AND branch, omitting known refs that already matched
|
|
4353
|
+
const branchOperands = [];
|
|
4354
|
+
if (unknown1) {
|
|
4355
|
+
branchOperands.push([op1, r1, r1Val]);
|
|
4356
|
+
}
|
|
4357
|
+
if (unknown2) {
|
|
4358
|
+
branchOperands.push([op2, r2, r2Val]);
|
|
4359
|
+
}
|
|
4360
|
+
if (branchOperands.length === 1) {
|
|
4361
|
+
branches.push(branchOperands[0]);
|
|
4362
|
+
} else {
|
|
4363
|
+
branches.push([andOp, ...branchOperands]);
|
|
4364
|
+
}
|
|
4365
|
+
}
|
|
4366
|
+
// Handle edge cases: no matches → false, single match → unwrap OR
|
|
4367
|
+
if (branches.length === 1) {
|
|
4368
|
+
// No entries matched
|
|
4369
|
+
stack[++stackTop] = false;
|
|
4370
|
+
} else if (branches.length === 2) {
|
|
4371
|
+
// Only one entry matched — unwrap the OR
|
|
4372
|
+
stack[++stackTop] = branches[1];
|
|
4373
|
+
} else {
|
|
4374
|
+
stack[++stackTop] = makeResidual(branches);
|
|
4264
4375
|
}
|
|
4265
|
-
stack[++stackTop] = makeResidual(branches);
|
|
4266
4376
|
break;
|
|
4267
4377
|
}
|
|
4268
4378
|
|
|
@@ -4270,8 +4380,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4270
4380
|
let found = false;
|
|
4271
4381
|
if (v1 !== null && v1 !== undefined && v2 !== null && v2 !== undefined) {
|
|
4272
4382
|
for (let j = 0; j < n; j++) {
|
|
4273
|
-
if (bytecode[
|
|
4274
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4383
|
+
if (bytecode[quadsStart + j * 4] === v1) {
|
|
4384
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4275
4385
|
let s = overlapSetCache.get(setB);
|
|
4276
4386
|
if (s === undefined) {
|
|
4277
4387
|
s = new Set(setB);
|
|
@@ -5147,6 +5257,11 @@ class Engine {
|
|
|
5147
5257
|
_defineProperty(this, "parser", void 0);
|
|
5148
5258
|
_defineProperty(this, "evaluator", void 0);
|
|
5149
5259
|
_defineProperty(this, "bytecodeCache", new WeakMap());
|
|
5260
|
+
// Separate cache for the simplify compiler output. Simplify must preserve the
|
|
5261
|
+
// nested structure verbatim, so it compiles with the OR_AND_IN merge disabled
|
|
5262
|
+
// (see `compile`'s `simplify` flag), which yields different bytecode than the
|
|
5263
|
+
// evaluate path and therefore cannot share `bytecodeCache`.
|
|
5264
|
+
_defineProperty(this, "simplifyCache", new WeakMap());
|
|
5150
5265
|
this.parser = new Parser(options);
|
|
5151
5266
|
this.evaluator = options?.evaluator ?? 'oop';
|
|
5152
5267
|
}
|
|
@@ -5160,6 +5275,20 @@ class Engine {
|
|
|
5160
5275
|
return compiled;
|
|
5161
5276
|
}
|
|
5162
5277
|
|
|
5278
|
+
/**
|
|
5279
|
+
* Compile for simplify: preserves the original nested structure so the
|
|
5280
|
+
* simplify interpreter can reproduce the input verbatim (no OR_AND_IN merge).
|
|
5281
|
+
*/
|
|
5282
|
+
getSimplifiedCompiled(exp) {
|
|
5283
|
+
let compiled = this.simplifyCache.get(exp);
|
|
5284
|
+
if (compiled === undefined) {
|
|
5285
|
+
this.parser.parse(exp); // validates root operator and expression structure
|
|
5286
|
+
compiled = compile(exp, this.parser.options, true);
|
|
5287
|
+
this.simplifyCache.set(exp, compiled);
|
|
5288
|
+
}
|
|
5289
|
+
return compiled;
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5163
5292
|
/**
|
|
5164
5293
|
* Evaluate the expression.
|
|
5165
5294
|
* @param {ExpressionInput} exp Raw expression.
|
|
@@ -5214,7 +5343,7 @@ class Engine {
|
|
|
5214
5343
|
*/
|
|
5215
5344
|
simplify(exp, context, strictKeys, optionalKeys) {
|
|
5216
5345
|
if (this.evaluator === 'bytecode') {
|
|
5217
|
-
return interpretSimplify(this.
|
|
5346
|
+
return interpretSimplify(this.getSimplifiedCompiled(exp), context, strictKeys, optionalKeys);
|
|
5218
5347
|
}
|
|
5219
5348
|
const result = this.parser.parse(exp).simplify(context, strictKeys, optionalKeys);
|
|
5220
5349
|
if (isEvaluable(result)) {
|
package/lib/illogical.esm.js
CHANGED
|
@@ -1300,7 +1300,7 @@ class Present extends Comparison {
|
|
|
1300
1300
|
}
|
|
1301
1301
|
|
|
1302
1302
|
// Operator key
|
|
1303
|
-
const OPERATOR$6 = Symbol('
|
|
1303
|
+
const OPERATOR$6 = Symbol('SUFFIX');
|
|
1304
1304
|
|
|
1305
1305
|
/**
|
|
1306
1306
|
* Suffix comparison expression
|
|
@@ -2289,7 +2289,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2289
2289
|
return {
|
|
2290
2290
|
rawRef,
|
|
2291
2291
|
refKey,
|
|
2292
|
-
vals: right
|
|
2292
|
+
vals: right,
|
|
2293
|
+
operator: 'in'
|
|
2293
2294
|
};
|
|
2294
2295
|
}
|
|
2295
2296
|
|
|
@@ -2305,7 +2306,8 @@ function extractInLikeChild(ca, state) {
|
|
|
2305
2306
|
return {
|
|
2306
2307
|
rawRef,
|
|
2307
2308
|
refKey,
|
|
2308
|
-
vals: [right]
|
|
2309
|
+
vals: [right],
|
|
2310
|
+
operator: 'eq'
|
|
2309
2311
|
};
|
|
2310
2312
|
}
|
|
2311
2313
|
return null;
|
|
@@ -2337,6 +2339,10 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2337
2339
|
|
|
2338
2340
|
// Inverted index: each distinct setA value → merged setB values across all branches containing it
|
|
2339
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();
|
|
2340
2346
|
for (let b = 1; b <= nBranches; b++) {
|
|
2341
2347
|
const branch = arr[b];
|
|
2342
2348
|
if (!Array.isArray(branch)) {
|
|
@@ -2392,6 +2398,20 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2392
2398
|
setBVals = new Set();
|
|
2393
2399
|
setBValsByAValue.set(aVal, setBVals);
|
|
2394
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);
|
|
2395
2415
|
for (const bVal of extB.vals) {
|
|
2396
2416
|
setBVals.add(bVal);
|
|
2397
2417
|
}
|
|
@@ -2403,14 +2423,25 @@ function detectOrAndIn2Pattern(arr, state) {
|
|
|
2403
2423
|
|
|
2404
2424
|
// Build entries: one (literal aVal, mergedSetBIdx) per distinct setA value
|
|
2405
2425
|
const entries = [];
|
|
2426
|
+
// Track operators per entry: [ref1Op, ref2Op]
|
|
2427
|
+
const entryOperators = [];
|
|
2406
2428
|
for (const [aVal, setBVals] of setBValsByAValue) {
|
|
2407
2429
|
const mergedSetB = [...setBVals].filter(v => v !== undefined);
|
|
2408
|
-
|
|
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]);
|
|
2409
2439
|
}
|
|
2410
2440
|
return {
|
|
2411
2441
|
ref1Raw,
|
|
2412
2442
|
ref2Raw,
|
|
2413
|
-
entries
|
|
2443
|
+
entries,
|
|
2444
|
+
entryOperators
|
|
2414
2445
|
};
|
|
2415
2446
|
}
|
|
2416
2447
|
|
|
@@ -2463,23 +2494,29 @@ function emitExpression(raw, state) {
|
|
|
2463
2494
|
return;
|
|
2464
2495
|
}
|
|
2465
2496
|
if (operator === maps.orOp) {
|
|
2466
|
-
|
|
2497
|
+
// Skip the merge when compiling for simplify so the nested structure can
|
|
2498
|
+
// be reconstructed verbatim (the merge collapses branches by ref1 value).
|
|
2499
|
+
const orAnd2 = state.simplify ? null : detectOrAndIn2Pattern(arr, state);
|
|
2467
2500
|
if (orAnd2 !== null) {
|
|
2468
2501
|
const {
|
|
2469
2502
|
ref1Raw,
|
|
2470
2503
|
ref2Raw,
|
|
2471
|
-
entries
|
|
2504
|
+
entries,
|
|
2505
|
+
entryOperators
|
|
2472
2506
|
} = orAnd2;
|
|
2473
2507
|
const {
|
|
2474
2508
|
bytecode
|
|
2475
2509
|
} = state;
|
|
2476
2510
|
const ref1Idx = internRef(ref1Raw, state);
|
|
2477
2511
|
const ref2Idx = internRef(ref2Raw, state);
|
|
2478
|
-
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M
|
|
2512
|
+
// Emit: OP_OR_AND_IN_CONST_2 ref1Idx ref2Idx M (aVal0 setBIdx0 ref1Op0 ref2Op0) ...
|
|
2479
2513
|
// M is the number of distinct setA values across all branches (after inverted-index merge).
|
|
2514
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
2480
2515
|
bytecode.push(OP_OR_AND_IN_CONST_2, ref1Idx, ref2Idx, entries.length);
|
|
2481
|
-
for (
|
|
2482
|
-
|
|
2516
|
+
for (let j = 0; j < entries.length; j++) {
|
|
2517
|
+
const [aVal, mergedSetBIdx] = entries[j];
|
|
2518
|
+
const [ref1Op, ref2Op] = entryOperators[j];
|
|
2519
|
+
bytecode.push(aVal, mergedSetBIdx, ref1Op === 'in' ? 1 : 0, ref2Op === 'in' ? 1 : 0);
|
|
2483
2520
|
}
|
|
2484
2521
|
return;
|
|
2485
2522
|
}
|
|
@@ -2736,7 +2773,7 @@ function emitExpression(raw, state) {
|
|
|
2736
2773
|
* Compile a raw ExpressionInput into bytecode.
|
|
2737
2774
|
* The result should be cached and reused across evaluate() calls.
|
|
2738
2775
|
*/
|
|
2739
|
-
function compile(raw, opts) {
|
|
2776
|
+
function compile(raw, opts, simplify = false) {
|
|
2740
2777
|
const maps = buildOperatorMaps(opts);
|
|
2741
2778
|
const state = {
|
|
2742
2779
|
bytecode: [],
|
|
@@ -2751,7 +2788,8 @@ function compile(raw, opts) {
|
|
|
2751
2788
|
consts: [],
|
|
2752
2789
|
constIndex: new Map(),
|
|
2753
2790
|
overlapRefsEntries: [],
|
|
2754
|
-
directionEntries: []
|
|
2791
|
+
directionEntries: [],
|
|
2792
|
+
simplify
|
|
2755
2793
|
};
|
|
2756
2794
|
emitExpression(raw, state);
|
|
2757
2795
|
|
|
@@ -3211,20 +3249,22 @@ function interpret(compiled, ctx) {
|
|
|
3211
3249
|
}
|
|
3212
3250
|
case OP_OR_AND_IN_CONST_2:
|
|
3213
3251
|
{
|
|
3214
|
-
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3252
|
+
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3253
|
+
// (aVal0, setBIdx0, ref1Op0, ref2Op0), (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
3254
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in' (unused at runtime, kept for simplifier)
|
|
3215
3255
|
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3216
3256
|
const ref1Idx = numAt$1(bytecode[++i]);
|
|
3217
3257
|
const ref2Idx = numAt$1(bytecode[++i]);
|
|
3218
3258
|
const m = numAt$1(bytecode[++i]);
|
|
3219
3259
|
const entriesStart = i + 1;
|
|
3220
|
-
i += m *
|
|
3260
|
+
i += m * 4;
|
|
3221
3261
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
3222
3262
|
const v2 = resolveCompactRef(refs[ref2Idx], ctx);
|
|
3223
3263
|
let found = false;
|
|
3224
3264
|
if (v1 !== undefined && v1 !== null && v2 !== undefined && v2 !== null) {
|
|
3225
3265
|
for (let j = 0; j < m; j++) {
|
|
3226
|
-
if (bytecode[entriesStart + j *
|
|
3227
|
-
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);
|
|
3228
3268
|
break;
|
|
3229
3269
|
}
|
|
3230
3270
|
}
|
|
@@ -3329,8 +3369,14 @@ function interpret(compiled, ctx) {
|
|
|
3329
3369
|
break;
|
|
3330
3370
|
}
|
|
3331
3371
|
const values = new Array(n);
|
|
3372
|
+
// Fixed destination slot for the single result value: the bottom-most
|
|
3373
|
+
// operand slot. The operands occupy [stackTop - n + 1 .. stackTop].
|
|
3374
|
+
// Writing here (instead of to stack[++stackTop]) keeps the result in a
|
|
3375
|
+
// stable slot even when the loop below breaks early on a missing
|
|
3376
|
+
// operand, so the next opcode never reads a stale operand value.
|
|
3377
|
+
const resultSlot = stackTop$1 - n + 1;
|
|
3332
3378
|
let hasNull = false;
|
|
3333
|
-
const isDateArithmetic = !isNaN(toDateNumber(stack$1[
|
|
3379
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[resultSlot])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3334
3380
|
for (let j = n - 1; j >= 0; j--) {
|
|
3335
3381
|
const v = stack$1[stackTop$1--];
|
|
3336
3382
|
if (v === null || v === undefined) {
|
|
@@ -3351,7 +3397,9 @@ function interpret(compiled, ctx) {
|
|
|
3351
3397
|
values[j] = v;
|
|
3352
3398
|
}
|
|
3353
3399
|
}
|
|
3354
|
-
|
|
3400
|
+
const result = hasNull ? false : isDateArithmetic && (op === OP_SUM || op === OP_SUBTRACT) && values.every(v => isString(v)) ? dateArithmeticReduce$1(values, op) : values.every(v => isNumber(v)) ? arithmeticReduce$1(values, op) : false;
|
|
3401
|
+
stack$1[resultSlot] = result;
|
|
3402
|
+
stackTop$1 = resultSlot;
|
|
3355
3403
|
break;
|
|
3356
3404
|
}
|
|
3357
3405
|
|
|
@@ -4231,13 +4279,15 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4231
4279
|
}
|
|
4232
4280
|
case OP_OR_AND_IN_CONST_2:
|
|
4233
4281
|
{
|
|
4234
|
-
// bytecode layout: ref1Idx, ref2Idx, M, aVal0, setBIdx0,
|
|
4282
|
+
// bytecode layout: ref1Idx, ref2Idx, M, (aVal0, setBIdx0, ref1Op0, ref2Op0),
|
|
4283
|
+
// (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
4235
4284
|
// aVal_j is a literal value; setBIdx_j is a constIdx for the merged setB.
|
|
4285
|
+
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in'
|
|
4236
4286
|
const ref1Idx = numAt(bytecode[i++]);
|
|
4237
4287
|
const ref2Idx = numAt(bytecode[i++]);
|
|
4238
4288
|
const n = numAt(bytecode[i++]);
|
|
4239
|
-
const
|
|
4240
|
-
i += n *
|
|
4289
|
+
const quadsStart = i;
|
|
4290
|
+
i += n * 4;
|
|
4241
4291
|
const rawKey1 = refRawKeys[ref1Idx];
|
|
4242
4292
|
const rawKey2 = refRawKeys[ref2Idx];
|
|
4243
4293
|
const v1 = resolveCompactRef(refs[ref1Idx], ctx);
|
|
@@ -4246,6 +4296,7 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4246
4296
|
const unknown2 = v2 === undefined && !strictSet?.has(rawKey2) && (!optionalSet || optionalSet.has(rawKey2));
|
|
4247
4297
|
if (unknown1 || unknown2) {
|
|
4248
4298
|
// Reconstruct the original complex expression tree
|
|
4299
|
+
// When one ref is known, only include entries where the known ref matches
|
|
4249
4300
|
const branches = [opNames[OP_OR]];
|
|
4250
4301
|
const andOp = opNames[OP_AND];
|
|
4251
4302
|
const eqOp = opNames[OP_EQ];
|
|
@@ -4253,12 +4304,71 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4253
4304
|
const r1 = refKeys[ref1Idx];
|
|
4254
4305
|
const r2 = refKeys[ref2Idx];
|
|
4255
4306
|
for (let j = 0; j < n; j++) {
|
|
4256
|
-
const aVal = literalAt(bytecode[
|
|
4257
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4258
|
-
const
|
|
4259
|
-
|
|
4307
|
+
const aVal = literalAt(bytecode[quadsStart + j * 4]);
|
|
4308
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4309
|
+
const ref1OpByte = numAt(bytecode[quadsStart + j * 4 + 2]);
|
|
4310
|
+
const ref2OpByte = numAt(bytecode[quadsStart + j * 4 + 3]);
|
|
4311
|
+
// Use the original operators for both operands
|
|
4312
|
+
const op1 = ref1OpByte === 1 ? inOp : eqOp;
|
|
4313
|
+
const op2 = ref2OpByte === 1 ? inOp : eqOp;
|
|
4314
|
+
// When reconstructing == with a scalar, use the scalar value
|
|
4315
|
+
// When reconstructing IN with a scalar, wrap it in an array
|
|
4316
|
+
const r1Val = op1 === eqOp ? aVal : [aVal];
|
|
4317
|
+
let r2Val = setB;
|
|
4318
|
+
if (op2 === eqOp && Array.isArray(setB) && setB.length === 1) {
|
|
4319
|
+
r2Val = setB[0];
|
|
4320
|
+
}
|
|
4321
|
+
// Skip entries where the known ref doesn't match
|
|
4322
|
+
if (!unknown1 && v1 !== undefined && v1 !== null) {
|
|
4323
|
+
// ref1 is known — only include matching entries
|
|
4324
|
+
// For ==, check exact match; for IN, check if value is in set
|
|
4325
|
+
let matches = false;
|
|
4326
|
+
if (op1 === eqOp) {
|
|
4327
|
+
matches = v1 === aVal;
|
|
4328
|
+
} else {
|
|
4329
|
+
matches = Array.isArray(v1) ? v1.includes(aVal) : v1 === aVal;
|
|
4330
|
+
}
|
|
4331
|
+
if (!matches) {
|
|
4332
|
+
continue;
|
|
4333
|
+
}
|
|
4334
|
+
}
|
|
4335
|
+
if (!unknown2 && v2 !== undefined && v2 !== null) {
|
|
4336
|
+
// ref2 is known — only include matching entries
|
|
4337
|
+
let matches = false;
|
|
4338
|
+
if (op2 === eqOp) {
|
|
4339
|
+
matches = v2 === r2Val;
|
|
4340
|
+
} else {
|
|
4341
|
+
// setB is always an array here (it's a compiled const)
|
|
4342
|
+
matches = Array.isArray(v2) ? setB.some(item => item === v2) : v2 === r2Val;
|
|
4343
|
+
}
|
|
4344
|
+
if (!matches) {
|
|
4345
|
+
continue;
|
|
4346
|
+
}
|
|
4347
|
+
}
|
|
4348
|
+
// Build the AND branch, omitting known refs that already matched
|
|
4349
|
+
const branchOperands = [];
|
|
4350
|
+
if (unknown1) {
|
|
4351
|
+
branchOperands.push([op1, r1, r1Val]);
|
|
4352
|
+
}
|
|
4353
|
+
if (unknown2) {
|
|
4354
|
+
branchOperands.push([op2, r2, r2Val]);
|
|
4355
|
+
}
|
|
4356
|
+
if (branchOperands.length === 1) {
|
|
4357
|
+
branches.push(branchOperands[0]);
|
|
4358
|
+
} else {
|
|
4359
|
+
branches.push([andOp, ...branchOperands]);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4362
|
+
// Handle edge cases: no matches → false, single match → unwrap OR
|
|
4363
|
+
if (branches.length === 1) {
|
|
4364
|
+
// No entries matched
|
|
4365
|
+
stack[++stackTop] = false;
|
|
4366
|
+
} else if (branches.length === 2) {
|
|
4367
|
+
// Only one entry matched — unwrap the OR
|
|
4368
|
+
stack[++stackTop] = branches[1];
|
|
4369
|
+
} else {
|
|
4370
|
+
stack[++stackTop] = makeResidual(branches);
|
|
4260
4371
|
}
|
|
4261
|
-
stack[++stackTop] = makeResidual(branches);
|
|
4262
4372
|
break;
|
|
4263
4373
|
}
|
|
4264
4374
|
|
|
@@ -4266,8 +4376,8 @@ function interpretSimplify(compiled, ctx, strictKeys, optionalKeys) {
|
|
|
4266
4376
|
let found = false;
|
|
4267
4377
|
if (v1 !== null && v1 !== undefined && v2 !== null && v2 !== undefined) {
|
|
4268
4378
|
for (let j = 0; j < n; j++) {
|
|
4269
|
-
if (bytecode[
|
|
4270
|
-
const setB = compiled.consts[numAt(bytecode[
|
|
4379
|
+
if (bytecode[quadsStart + j * 4] === v1) {
|
|
4380
|
+
const setB = compiled.consts[numAt(bytecode[quadsStart + j * 4 + 1])];
|
|
4271
4381
|
let s = overlapSetCache.get(setB);
|
|
4272
4382
|
if (s === undefined) {
|
|
4273
4383
|
s = new Set(setB);
|
|
@@ -5143,6 +5253,11 @@ class Engine {
|
|
|
5143
5253
|
_defineProperty(this, "parser", void 0);
|
|
5144
5254
|
_defineProperty(this, "evaluator", void 0);
|
|
5145
5255
|
_defineProperty(this, "bytecodeCache", new WeakMap());
|
|
5256
|
+
// Separate cache for the simplify compiler output. Simplify must preserve the
|
|
5257
|
+
// nested structure verbatim, so it compiles with the OR_AND_IN merge disabled
|
|
5258
|
+
// (see `compile`'s `simplify` flag), which yields different bytecode than the
|
|
5259
|
+
// evaluate path and therefore cannot share `bytecodeCache`.
|
|
5260
|
+
_defineProperty(this, "simplifyCache", new WeakMap());
|
|
5146
5261
|
this.parser = new Parser(options);
|
|
5147
5262
|
this.evaluator = options?.evaluator ?? 'oop';
|
|
5148
5263
|
}
|
|
@@ -5156,6 +5271,20 @@ class Engine {
|
|
|
5156
5271
|
return compiled;
|
|
5157
5272
|
}
|
|
5158
5273
|
|
|
5274
|
+
/**
|
|
5275
|
+
* Compile for simplify: preserves the original nested structure so the
|
|
5276
|
+
* simplify interpreter can reproduce the input verbatim (no OR_AND_IN merge).
|
|
5277
|
+
*/
|
|
5278
|
+
getSimplifiedCompiled(exp) {
|
|
5279
|
+
let compiled = this.simplifyCache.get(exp);
|
|
5280
|
+
if (compiled === undefined) {
|
|
5281
|
+
this.parser.parse(exp); // validates root operator and expression structure
|
|
5282
|
+
compiled = compile(exp, this.parser.options, true);
|
|
5283
|
+
this.simplifyCache.set(exp, compiled);
|
|
5284
|
+
}
|
|
5285
|
+
return compiled;
|
|
5286
|
+
}
|
|
5287
|
+
|
|
5159
5288
|
/**
|
|
5160
5289
|
* Evaluate the expression.
|
|
5161
5290
|
* @param {ExpressionInput} exp Raw expression.
|
|
@@ -5210,7 +5339,7 @@ class Engine {
|
|
|
5210
5339
|
*/
|
|
5211
5340
|
simplify(exp, context, strictKeys, optionalKeys) {
|
|
5212
5341
|
if (this.evaluator === 'bytecode') {
|
|
5213
|
-
return interpretSimplify(this.
|
|
5342
|
+
return interpretSimplify(this.getSimplifiedCompiled(exp), context, strictKeys, optionalKeys);
|
|
5214
5343
|
}
|
|
5215
5344
|
const result = this.parser.parse(exp).simplify(context, strictKeys, optionalKeys);
|
|
5216
5345
|
if (isEvaluable(result)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@briza/illogical",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.4",
|
|
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",
|
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
"build:types": "tsc --project tsconfig.build.json --emitDeclarationOnly",
|
|
25
25
|
"build:js": "rollup -c",
|
|
26
26
|
"build": "rm -rf lib types && npm run build:types && npm run build:js",
|
|
27
|
-
"
|
|
28
|
-
"test": "node --import tsx --test \"src/**/*.test.ts\"",
|
|
27
|
+
"test": "bash -c 'node --import tsx --test \"${@:-src/__test__/unit/**/*.test.ts}\"' --",
|
|
29
28
|
"test:sample-conditions": "node --import tsx --test --test-reporter=spec \"src/__test__/unit/sample-conditions.test.ts\"",
|
|
30
|
-
"test:coverage": "node --import tsx --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage.lcov \"src/**/*.test.ts\"",
|
|
29
|
+
"test:coverage": "node --import tsx --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage.lcov \"src/__test__/unit/**/*.test.ts\"",
|
|
31
30
|
"lint": "eslint --max-warnings 0 \"src/**/*.{ts,js}\"",
|
|
32
31
|
"lint:fix": "eslint --max-warnings 0 \"src/**/*.{ts,js}\" --fix",
|
|
33
32
|
"prepublishOnly": "npm run test && npm run build",
|
|
@@ -53,8 +52,11 @@
|
|
|
53
52
|
"bench:synthetic:report:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-synthetic-simplify-oop.json benchmark/results-synthetic-simplify-bytecode.json --op simplify --out benchmark/report-synthetic-simplify.md",
|
|
54
53
|
"bench:sample:report:full:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-sample-simplify-oop.json benchmark/results-sample-simplify-bytecode.json --op simplify --full --out benchmark/report-sample-simplify-full.md",
|
|
55
54
|
"bench:synthetic:report:full:simplify": "node --import tsx src/benchmark/report.ts benchmark/results-synthetic-simplify-oop.json benchmark/results-synthetic-simplify-bytecode.json --op simplify --full --out benchmark/report-synthetic-simplify-full.md",
|
|
55
|
+
"bench:full:report:test-case": "sh -c 'FILTER=$(echo \"$@\" | sed -n \"s/.*--filter \\([^ ]*\\).*/\\1/p\"); if [ -z \"$FILTER\" ]; then FILTER=\"tmp\"; fi; node --import tsx src/benchmark/simplify.ts --cases conditions/sample-conditions \"$@\" --out benchmark/results-simplify-${FILTER}-oop.json && node --import tsx src/benchmark/simplify.ts --cases conditions/sample-conditions \"$@\" --options '\\''{\"evaluator\":\"bytecode\"}'\\'' --out benchmark/results-simplify-${FILTER}-bytecode.json && node --import tsx src/benchmark/report.ts benchmark/results-simplify-${FILTER}-oop.json benchmark/results-simplify-${FILTER}-bytecode.json --op simplify --out benchmark/report-simplify-${FILTER}.md && echo \"\\nReport written to benchmark/report-simplify-${FILTER}.md\" && node --import tsx src/benchmark/evaluate.ts --cases conditions/sample-conditions \"$@\" --out benchmark/results-evaluate-${FILTER}-oop.json && node --import tsx src/benchmark/evaluate.ts --cases conditions/sample-conditions \"$@\" --options '\\''{\"evaluator\":\"bytecode\"}'\\'' --out benchmark/results-evaluate-${FILTER}-bytecode.json && node --import tsx src/benchmark/report.ts benchmark/results-evaluate-${FILTER}-oop.json benchmark/results-evaluate-${FILTER}-bytecode.json --op evaluate --out benchmark/report-evaluate-${FILTER}.md && echo \"\\nReport written to benchmark/report-evaluate-${FILTER}.md\"' -- ",
|
|
56
56
|
"get-bytecode": "node --import tsx src/bytecode/get-bytecode.ts",
|
|
57
|
-
"debug-bytecode": "node --import tsx src/tools/debugger.ts"
|
|
57
|
+
"debug-bytecode": "node --import tsx src/tools/debugger.ts",
|
|
58
|
+
"test:fuzz": "node --import tsx src/tools/fuzz-runner.ts",
|
|
59
|
+
"test:fuzz:deep": "FUZZ_RUNS=100000 npm run test:fuzz"
|
|
58
60
|
},
|
|
59
61
|
"repository": {
|
|
60
62
|
"type": "git",
|
|
@@ -92,12 +94,12 @@
|
|
|
92
94
|
"eslint-plugin-prettier": "^5.5.5",
|
|
93
95
|
"eslint-plugin-promise": "^7.2.1",
|
|
94
96
|
"eslint-plugin-simple-import-sort": "^12.1.1",
|
|
97
|
+
"fast-check": "^4.8.0",
|
|
95
98
|
"license-checker": "^25.0.1",
|
|
96
99
|
"prettier": "^3.6.2",
|
|
97
100
|
"rollup": "^4.52.5",
|
|
98
101
|
"tinybench": "^6.0.0",
|
|
99
102
|
"tsx": "^4.19.1",
|
|
100
|
-
"typedoc": "^0.28.18",
|
|
101
103
|
"typescript": "^6.0.2"
|
|
102
104
|
}
|
|
103
105
|
}
|
package/readme.md
CHANGED
|
@@ -13,11 +13,13 @@
|
|
|
13
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
14
|
</p>
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
+
|
|
21
23
|
</div>
|
|
22
24
|
|
|
23
25
|
---
|
|
@@ -26,6 +28,8 @@
|
|
|
26
28
|
|
|
27
29
|
Get up and running with illogical in just a few steps.
|
|
28
30
|
|
|
31
|
+
Read the [Background](#background) section to quickly understand what illogical is and why it exists.
|
|
32
|
+
|
|
29
33
|
### Installation
|
|
30
34
|
|
|
31
35
|
```sh
|
|
@@ -67,6 +71,18 @@ engine.evaluate(['AND', ['>', '$age', 20], ['==', '$name', 'peter']]) // true
|
|
|
67
71
|
|
|
68
72
|
## 📚 Documentation
|
|
69
73
|
|
|
74
|
+
### Background
|
|
75
|
+
|
|
76
|
+
**illogical** is a JSON DSL (domain-specific language) for expressing and evaluating business rules in the insurance industry.
|
|
77
|
+
|
|
78
|
+
Domain-specific languages exist to streamline work in a given domain by providing the means of performing tasks in a way that is quicker to learn and implement. This lets you optimize your custom solution and tailor it for use by distinct user groups or within unique contexts.
|
|
79
|
+
|
|
80
|
+
Developers of software for insurance underwriters can enable illogical quickly and easily to make the underwriter's task of writing question sets for programmatic use more efficient by eliminating the need to learn and understand the programming language(s) in use.
|
|
81
|
+
|
|
82
|
+
The way it works is that underwriters use the illogical JSON DSL to express their underwriting models, specifically their question sets and business rules. Then, the JavaScript functions in illogical parse what has been written for use within an application.
|
|
83
|
+
|
|
84
|
+
Another way to think about this is that illogical is used to define machine-readable business rules for underwriting models and question sets that brokers and distribution partners can easily consume. This is much more efficient than the traditional use of spreadsheets, documents, PDFs, email exchanges, and other non-machine-readable formats that then must be translated before being used. illogical speeds up the integration process, gets your rules to market faster, and does so with significantly reduced submission errors and a lower total cost of ownership.
|
|
85
|
+
|
|
70
86
|
### Core Concepts
|
|
71
87
|
|
|
72
88
|
Explore the supported expressions and their usage:
|
|
@@ -88,10 +104,7 @@ Learn how to use the engine and its methods:
|
|
|
88
104
|
|
|
89
105
|
### Customization
|
|
90
106
|
|
|
91
|
-
Customize the engine
|
|
92
|
-
|
|
93
|
-
- [Engine Options](./specs/engine.md)
|
|
94
|
-
- [Code Documentation](https://briza-insurance.github.io/illogical/index.html)
|
|
107
|
+
Customize the [engine options](./specs/engine.md).
|
|
95
108
|
|
|
96
109
|
### Development Tools
|
|
97
110
|
|
|
@@ -10,6 +10,63 @@ import { ArrayInput, ExpressionInput, Input } from '../parser/index.js';
|
|
|
10
10
|
import { Options } from '../parser/options.js';
|
|
11
11
|
import { CompactRef } from './refs.js';
|
|
12
12
|
export type Bytecode = (number | Result)[];
|
|
13
|
+
interface OperatorMaps {
|
|
14
|
+
binary: Record<string, number>;
|
|
15
|
+
arithmetic: Record<string, number>;
|
|
16
|
+
presentOp: string;
|
|
17
|
+
undefinedOp: string;
|
|
18
|
+
andOp: string;
|
|
19
|
+
orOp: string;
|
|
20
|
+
norOp: string;
|
|
21
|
+
notOp: string;
|
|
22
|
+
xorOp: string;
|
|
23
|
+
inOp: string;
|
|
24
|
+
notInOp: string;
|
|
25
|
+
overlapOp: string;
|
|
26
|
+
eqOp: string;
|
|
27
|
+
}
|
|
28
|
+
export interface CompilerState {
|
|
29
|
+
bytecode: Bytecode;
|
|
30
|
+
refs: CompactRef[];
|
|
31
|
+
refIndex: Map<string, number>;
|
|
32
|
+
refRawKeys: string[];
|
|
33
|
+
refKeys: string[];
|
|
34
|
+
opts: Options;
|
|
35
|
+
maps: OperatorMaps;
|
|
36
|
+
collectionCse: Map<string, number>;
|
|
37
|
+
numLocals: number;
|
|
38
|
+
consts: ArrayInput[];
|
|
39
|
+
constIndex: Map<string, number>;
|
|
40
|
+
overlapRefsEntries: Array<{
|
|
41
|
+
pos: number;
|
|
42
|
+
refIdxs: number[];
|
|
43
|
+
}>;
|
|
44
|
+
directionEntries: Array<{
|
|
45
|
+
pos: number;
|
|
46
|
+
dir: 0 | 1;
|
|
47
|
+
}>;
|
|
48
|
+
simplify: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Check whether an OR expression matches the pattern:
|
|
52
|
+
* OR( AND(IN-like(ref1, set1), IN-like(ref2, set2)), ... )
|
|
53
|
+
* where IN-like is either IN(ref, staticSet) or ==(ref, scalar),
|
|
54
|
+
* and every branch uses the exact same two refs in the same order.
|
|
55
|
+
*
|
|
56
|
+
* Builds an inverted index: for each unique value in any setA, union-merges all
|
|
57
|
+
* setB values across branches where that setA value appears, and emits one
|
|
58
|
+
* (literal value, mergedSetBIdx) entry per distinct setA value.
|
|
59
|
+
*
|
|
60
|
+
* This lets the interpreter do a single O(1) Map lookup on ref1 to find all
|
|
61
|
+
* relevant setB indices, instead of a linear scan through N setA Sets.
|
|
62
|
+
* Returns null if the pattern does not match.
|
|
63
|
+
*/
|
|
64
|
+
export declare function detectOrAndIn2Pattern(arr: ArrayInput, state: CompilerState): {
|
|
65
|
+
ref1Raw: string;
|
|
66
|
+
ref2Raw: string;
|
|
67
|
+
entries: Array<[Result, number]>;
|
|
68
|
+
entryOperators: Array<['eq' | 'in', 'eq' | 'in']>;
|
|
69
|
+
} | null;
|
|
13
70
|
export interface CompiledExpression {
|
|
14
71
|
bytecode: Bytecode;
|
|
15
72
|
refs: CompactRef[];
|
|
@@ -26,4 +83,5 @@ export interface CompiledExpression {
|
|
|
26
83
|
* Compile a raw ExpressionInput into bytecode.
|
|
27
84
|
* The result should be cached and reused across evaluate() calls.
|
|
28
85
|
*/
|
|
29
|
-
export declare function compile(raw: ExpressionInput, opts: Options): CompiledExpression;
|
|
86
|
+
export declare function compile(raw: ExpressionInput, opts: Options, simplify?: boolean): CompiledExpression;
|
|
87
|
+
export {};
|
|
@@ -3,7 +3,7 @@ import { Options } from '../parser/options.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Valid types for context members
|
|
5
5
|
*/
|
|
6
|
-
type ContextValue = Record<string, unknown> | string | number | boolean | null | undefined | ContextValue[];
|
|
6
|
+
export type ContextValue = Record<string, unknown> | string | number | boolean | null | undefined | ContextValue[];
|
|
7
7
|
/**
|
|
8
8
|
* Evaluation Context
|
|
9
9
|
* Holds references used during the evaluation process.
|
|
@@ -56,4 +56,3 @@ export interface Evaluable {
|
|
|
56
56
|
toString(): string;
|
|
57
57
|
}
|
|
58
58
|
export type SimplifyArgs = Parameters<Evaluable['simplify']>;
|
|
59
|
-
export {};
|
package/types/index.d.ts
CHANGED
|
@@ -39,12 +39,18 @@ declare class Engine {
|
|
|
39
39
|
private readonly parser;
|
|
40
40
|
private readonly evaluator;
|
|
41
41
|
private readonly bytecodeCache;
|
|
42
|
+
private readonly simplifyCache;
|
|
42
43
|
/**
|
|
43
44
|
* @constructor
|
|
44
45
|
* @param {Options?} options Parser options.
|
|
45
46
|
*/
|
|
46
47
|
constructor(options?: Partial<Options>);
|
|
47
48
|
private getCompiled;
|
|
49
|
+
/**
|
|
50
|
+
* Compile for simplify: preserves the original nested structure so the
|
|
51
|
+
* simplify interpreter can reproduce the input verbatim (no OR_AND_IN merge).
|
|
52
|
+
*/
|
|
53
|
+
private getSimplifiedCompiled;
|
|
48
54
|
/**
|
|
49
55
|
* Evaluate the expression.
|
|
50
56
|
* @param {ExpressionInput} exp Raw expression.
|