@briza/illogical 2.2.3 → 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 +39 -8
- package/lib/illogical.esm.js +39 -8
- package/package.json +8 -6
- package/readme.md +15 -4
- 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
|
|
@@ -2498,7 +2498,9 @@ function emitExpression(raw, state) {
|
|
|
2498
2498
|
return;
|
|
2499
2499
|
}
|
|
2500
2500
|
if (operator === maps.orOp) {
|
|
2501
|
-
|
|
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);
|
|
2502
2504
|
if (orAnd2 !== null) {
|
|
2503
2505
|
const {
|
|
2504
2506
|
ref1Raw,
|
|
@@ -2775,7 +2777,7 @@ function emitExpression(raw, state) {
|
|
|
2775
2777
|
* Compile a raw ExpressionInput into bytecode.
|
|
2776
2778
|
* The result should be cached and reused across evaluate() calls.
|
|
2777
2779
|
*/
|
|
2778
|
-
function compile(raw, opts) {
|
|
2780
|
+
function compile(raw, opts, simplify = false) {
|
|
2779
2781
|
const maps = buildOperatorMaps(opts);
|
|
2780
2782
|
const state = {
|
|
2781
2783
|
bytecode: [],
|
|
@@ -2790,7 +2792,8 @@ function compile(raw, opts) {
|
|
|
2790
2792
|
consts: [],
|
|
2791
2793
|
constIndex: new Map(),
|
|
2792
2794
|
overlapRefsEntries: [],
|
|
2793
|
-
directionEntries: []
|
|
2795
|
+
directionEntries: [],
|
|
2796
|
+
simplify
|
|
2794
2797
|
};
|
|
2795
2798
|
emitExpression(raw, state);
|
|
2796
2799
|
|
|
@@ -3250,7 +3253,8 @@ function interpret(compiled, ctx) {
|
|
|
3250
3253
|
}
|
|
3251
3254
|
case OP_OR_AND_IN_CONST_2:
|
|
3252
3255
|
{
|
|
3253
|
-
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3256
|
+
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3257
|
+
// (aVal0, setBIdx0, ref1Op0, ref2Op0), (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
3254
3258
|
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in' (unused at runtime, kept for simplifier)
|
|
3255
3259
|
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3256
3260
|
const ref1Idx = numAt$1(bytecode[++i]);
|
|
@@ -3369,8 +3373,14 @@ function interpret(compiled, ctx) {
|
|
|
3369
3373
|
break;
|
|
3370
3374
|
}
|
|
3371
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;
|
|
3372
3382
|
let hasNull = false;
|
|
3373
|
-
const isDateArithmetic = !isNaN(toDateNumber(stack$1[
|
|
3383
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[resultSlot])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3374
3384
|
for (let j = n - 1; j >= 0; j--) {
|
|
3375
3385
|
const v = stack$1[stackTop$1--];
|
|
3376
3386
|
if (v === null || v === undefined) {
|
|
@@ -3391,7 +3401,9 @@ function interpret(compiled, ctx) {
|
|
|
3391
3401
|
values[j] = v;
|
|
3392
3402
|
}
|
|
3393
3403
|
}
|
|
3394
|
-
|
|
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;
|
|
3395
3407
|
break;
|
|
3396
3408
|
}
|
|
3397
3409
|
|
|
@@ -5245,6 +5257,11 @@ class Engine {
|
|
|
5245
5257
|
_defineProperty(this, "parser", void 0);
|
|
5246
5258
|
_defineProperty(this, "evaluator", void 0);
|
|
5247
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());
|
|
5248
5265
|
this.parser = new Parser(options);
|
|
5249
5266
|
this.evaluator = options?.evaluator ?? 'oop';
|
|
5250
5267
|
}
|
|
@@ -5258,6 +5275,20 @@ class Engine {
|
|
|
5258
5275
|
return compiled;
|
|
5259
5276
|
}
|
|
5260
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
|
+
|
|
5261
5292
|
/**
|
|
5262
5293
|
* Evaluate the expression.
|
|
5263
5294
|
* @param {ExpressionInput} exp Raw expression.
|
|
@@ -5312,7 +5343,7 @@ class Engine {
|
|
|
5312
5343
|
*/
|
|
5313
5344
|
simplify(exp, context, strictKeys, optionalKeys) {
|
|
5314
5345
|
if (this.evaluator === 'bytecode') {
|
|
5315
|
-
return interpretSimplify(this.
|
|
5346
|
+
return interpretSimplify(this.getSimplifiedCompiled(exp), context, strictKeys, optionalKeys);
|
|
5316
5347
|
}
|
|
5317
5348
|
const result = this.parser.parse(exp).simplify(context, strictKeys, optionalKeys);
|
|
5318
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
|
|
@@ -2494,7 +2494,9 @@ function emitExpression(raw, state) {
|
|
|
2494
2494
|
return;
|
|
2495
2495
|
}
|
|
2496
2496
|
if (operator === maps.orOp) {
|
|
2497
|
-
|
|
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);
|
|
2498
2500
|
if (orAnd2 !== null) {
|
|
2499
2501
|
const {
|
|
2500
2502
|
ref1Raw,
|
|
@@ -2771,7 +2773,7 @@ function emitExpression(raw, state) {
|
|
|
2771
2773
|
* Compile a raw ExpressionInput into bytecode.
|
|
2772
2774
|
* The result should be cached and reused across evaluate() calls.
|
|
2773
2775
|
*/
|
|
2774
|
-
function compile(raw, opts) {
|
|
2776
|
+
function compile(raw, opts, simplify = false) {
|
|
2775
2777
|
const maps = buildOperatorMaps(opts);
|
|
2776
2778
|
const state = {
|
|
2777
2779
|
bytecode: [],
|
|
@@ -2786,7 +2788,8 @@ function compile(raw, opts) {
|
|
|
2786
2788
|
consts: [],
|
|
2787
2789
|
constIndex: new Map(),
|
|
2788
2790
|
overlapRefsEntries: [],
|
|
2789
|
-
directionEntries: []
|
|
2791
|
+
directionEntries: [],
|
|
2792
|
+
simplify
|
|
2790
2793
|
};
|
|
2791
2794
|
emitExpression(raw, state);
|
|
2792
2795
|
|
|
@@ -3246,7 +3249,8 @@ function interpret(compiled, ctx) {
|
|
|
3246
3249
|
}
|
|
3247
3250
|
case OP_OR_AND_IN_CONST_2:
|
|
3248
3251
|
{
|
|
3249
|
-
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3252
|
+
// bytecode layout: ref1Idx, ref2Idx, M,
|
|
3253
|
+
// (aVal0, setBIdx0, ref1Op0, ref2Op0), (aVal1, setBIdx1, ref1Op1, ref2Op1), ...
|
|
3250
3254
|
// ref1Op/ref2Op: 0 for 'eq', 1 for 'in' (unused at runtime, kept for simplifier)
|
|
3251
3255
|
// constSets[setBIdx] is pre-built at first interpret() call — plain Set.has lookup.
|
|
3252
3256
|
const ref1Idx = numAt$1(bytecode[++i]);
|
|
@@ -3365,8 +3369,14 @@ function interpret(compiled, ctx) {
|
|
|
3365
3369
|
break;
|
|
3366
3370
|
}
|
|
3367
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;
|
|
3368
3378
|
let hasNull = false;
|
|
3369
|
-
const isDateArithmetic = !isNaN(toDateNumber(stack$1[
|
|
3379
|
+
const isDateArithmetic = !isNaN(toDateNumber(stack$1[resultSlot])) && (op === OP_SUM || op === OP_SUBTRACT);
|
|
3370
3380
|
for (let j = n - 1; j >= 0; j--) {
|
|
3371
3381
|
const v = stack$1[stackTop$1--];
|
|
3372
3382
|
if (v === null || v === undefined) {
|
|
@@ -3387,7 +3397,9 @@ function interpret(compiled, ctx) {
|
|
|
3387
3397
|
values[j] = v;
|
|
3388
3398
|
}
|
|
3389
3399
|
}
|
|
3390
|
-
|
|
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;
|
|
3391
3403
|
break;
|
|
3392
3404
|
}
|
|
3393
3405
|
|
|
@@ -5241,6 +5253,11 @@ class Engine {
|
|
|
5241
5253
|
_defineProperty(this, "parser", void 0);
|
|
5242
5254
|
_defineProperty(this, "evaluator", void 0);
|
|
5243
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());
|
|
5244
5261
|
this.parser = new Parser(options);
|
|
5245
5262
|
this.evaluator = options?.evaluator ?? 'oop';
|
|
5246
5263
|
}
|
|
@@ -5254,6 +5271,20 @@ class Engine {
|
|
|
5254
5271
|
return compiled;
|
|
5255
5272
|
}
|
|
5256
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
|
+
|
|
5257
5288
|
/**
|
|
5258
5289
|
* Evaluate the expression.
|
|
5259
5290
|
* @param {ExpressionInput} exp Raw expression.
|
|
@@ -5308,7 +5339,7 @@ class Engine {
|
|
|
5308
5339
|
*/
|
|
5309
5340
|
simplify(exp, context, strictKeys, optionalKeys) {
|
|
5310
5341
|
if (this.evaluator === 'bytecode') {
|
|
5311
|
-
return interpretSimplify(this.
|
|
5342
|
+
return interpretSimplify(this.getSimplifiedCompiled(exp), context, strictKeys, optionalKeys);
|
|
5312
5343
|
}
|
|
5313
5344
|
const result = this.parser.parse(exp).simplify(context, strictKeys, optionalKeys);
|
|
5314
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
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
Get up and running with illogical in just a few steps.
|
|
30
30
|
|
|
31
|
+
Read the [Background](#background) section to quickly understand what illogical is and why it exists.
|
|
32
|
+
|
|
31
33
|
### Installation
|
|
32
34
|
|
|
33
35
|
```sh
|
|
@@ -69,6 +71,18 @@ engine.evaluate(['AND', ['>', '$age', 20], ['==', '$name', 'peter']]) // true
|
|
|
69
71
|
|
|
70
72
|
## 📚 Documentation
|
|
71
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
|
+
|
|
72
86
|
### Core Concepts
|
|
73
87
|
|
|
74
88
|
Explore the supported expressions and their usage:
|
|
@@ -90,10 +104,7 @@ Learn how to use the engine and its methods:
|
|
|
90
104
|
|
|
91
105
|
### Customization
|
|
92
106
|
|
|
93
|
-
Customize the engine
|
|
94
|
-
|
|
95
|
-
- [Engine Options](./specs/engine.md)
|
|
96
|
-
- [Code Documentation](https://briza-insurance.github.io/illogical/index.html)
|
|
107
|
+
Customize the [engine options](./specs/engine.md).
|
|
97
108
|
|
|
98
109
|
### Development Tools
|
|
99
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.
|