Skip to content

Commit da5531a

Browse files
committed
Remove the Yielding stuff; since it really never ended up in use by anyone, especially myself. Also adds an optimizer for the interpreted logic for performance improvements. The optimizer is not fast as the compiler, but improves performance regardless (seeing 2-3x gains on .run logic).
1 parent e5ff00c commit da5531a

23 files changed

+389
-1651
lines changed

async.test.js

+3-68
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { AsyncLogicEngine } from './index.js'
2-
import YieldStructure from './structures/Yield.js'
3-
import EngineObject from './structures/EngineObject.js'
42

53
const modes = [
64
new AsyncLogicEngine(),
7-
new AsyncLogicEngine(undefined, {
8-
yieldSupported: true
9-
})
5+
new AsyncLogicEngine(undefined, { disableInterpretedOptimization: true })
106
]
117

128
modes.forEach((logic) => {
@@ -832,9 +828,9 @@ modes.forEach((logic) => {
832828
{
833829
var: 'toString'
834830
},
835-
{toString: 'hello'}
831+
{ toString: 'hello' }
836832
)
837-
).toBe("hello")
833+
).toBe('hello')
838834
})
839835

840836
test('allow access to functions on objects when enabled', async () => {
@@ -849,65 +845,4 @@ modes.forEach((logic) => {
849845
).toBe('hello'.toString)
850846
})
851847
})
852-
853-
if (logic.options.yieldSupported) {
854-
describe('Yield technology', () => {
855-
test('Yield if variable does not exist', async () => {
856-
logic.addMethod(
857-
'yieldVar',
858-
(key, context, above, engine) => {
859-
if (!key) return context
860-
861-
if (typeof context !== 'object' && key.startsWith('../')) {
862-
return engine.methods.var(
863-
key.substring(3),
864-
above,
865-
undefined,
866-
engine
867-
)
868-
}
869-
870-
if (engine.allowFunctions || typeof (context && context[key]) !== 'function') {
871-
if (!(key in context)) {
872-
return new YieldStructure({
873-
message: 'Data does not exist in context.'
874-
})
875-
}
876-
return context[key]
877-
}
878-
},
879-
{ sync: true }
880-
)
881-
882-
const script = {
883-
'+': [1, { '+': [1, 2, 3] }, { yieldVar: 'a' }]
884-
}
885-
886-
const instance = await logic.run(script)
887-
888-
expect(instance instanceof EngineObject).toBe(true)
889-
890-
expect(instance.yields().map((i) => ({ ...i }))).toStrictEqual([
891-
{
892-
_input: null,
893-
message: 'Data does not exist in context.',
894-
_logic: { yieldVar: 'a' },
895-
resumable: null
896-
}
897-
])
898-
899-
expect(instance.logic()).toStrictEqual({
900-
'+': [1, 6, { yieldVar: 'a' }]
901-
})
902-
903-
expect(
904-
await logic.run(instance.logic(), {
905-
a: 1
906-
})
907-
).toBe(8)
908-
909-
expect(await logic.run(script, { a: 1 })).toBe(8)
910-
})
911-
})
912-
}
913848
})

asyncLogic.js

+42-43
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
// @ts-check
22
'use strict'
33

4-
import checkYield from './utilities/checkYield.js'
54
import defaultMethods from './defaultMethods.js'
6-
import YieldStructure from './structures/Yield.js'
7-
import EngineObject from './structures/EngineObject.js'
85
import LogicEngine from './logic.js'
96
import asyncPool from './asyncPool.js'
107
import { Sync, isSync } from './constants.js'
118
import declareSync from './utilities/declareSync.js'
129
import { buildAsync } from './compiler.js'
1310
import omitUndefined from './utilities/omitUndefined.js'
11+
import { optimize } from './async_optimizer.js'
1412

1513
/**
1614
* An engine capable of running asynchronous JSON Logic.
@@ -19,18 +17,21 @@ class AsyncLogicEngine {
1917
/**
2018
*
2119
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
22-
* @param {{ yieldSupported?: Boolean, disableInline?: Boolean, permissive?: boolean }} options
20+
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: boolean, permissive?: boolean }} options
2321
*/
2422
constructor (
2523
methods = defaultMethods,
26-
options = { yieldSupported: false, disableInline: false, permissive: false }
24+
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false }
2725
) {
2826
this.methods = { ...methods }
29-
/** @type {{yieldSupported?: Boolean, disableInline?: Boolean }} */
30-
this.options = { yieldSupported: options.yieldSupported, disableInline: options.disableInline }
27+
/** @type {{disableInline?: Boolean, disableInterpretedOptimization?: Boolean }} */
28+
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization }
3129
this.disableInline = options.disableInline
30+
this.disableInterpretedOptimization = options.disableInterpretedOptimization
3231
this.async = true
3332
this.fallback = new LogicEngine(methods, options)
33+
this.optimizedMap = new WeakMap()
34+
this.missesSinceSeen = 0
3435

3536
if (!this.isData) {
3637
if (!options.permissive) this.isData = () => false
@@ -57,9 +58,7 @@ class AsyncLogicEngine {
5758

5859
if (typeof this.methods[func] === 'function') {
5960
const input = await this.run(data, context, { above })
60-
if (this.options.yieldSupported && (await checkYield(input))) {
61-
return { result: input, func }
62-
}
61+
6362
const result = await this.methods[func](input, context, above, this)
6463
return { result: Array.isArray(result) ? Promise.all(result) : result, func }
6564
}
@@ -72,10 +71,6 @@ class AsyncLogicEngine {
7271
? await this.run(data, context, { above })
7372
: data
7473

75-
if (this.options.yieldSupported && (await checkYield(parsedData))) {
76-
return { result: parsedData, func }
77-
}
78-
7974
const result = await (asyncMethod || method)(
8075
parsedData,
8176
context,
@@ -92,12 +87,12 @@ class AsyncLogicEngine {
9287
*
9388
* @param {String} name The name of the method being added.
9489
* @param {Function|{ traverse?: Boolean, method?: Function, asyncMethod?: Function, deterministic?: Function | Boolean }} method
95-
* @param {{ deterministic?: Boolean, yields?: Boolean, useContext?: Boolean, async?: Boolean, sync?: Boolean }} annotations This is used by the compiler to help determine if it can optimize the function being generated.
90+
* @param {{ deterministic?: Boolean, useContext?: Boolean, async?: Boolean, sync?: Boolean }} annotations This is used by the compiler to help determine if it can optimize the function being generated.
9691
*/
9792
addMethod (
9893
name,
9994
method,
100-
{ deterministic, async, sync, yields, useContext } = {}
95+
{ deterministic, async, sync, useContext } = {}
10196
) {
10297
if (typeof async === 'undefined' && typeof sync === 'undefined') sync = false
10398
if (typeof sync !== 'undefined') async = !sync
@@ -113,17 +108,17 @@ class AsyncLogicEngine {
113108
method = { ...method }
114109
}
115110

116-
Object.assign(method, omitUndefined({ yields, deterministic, useContext }))
111+
Object.assign(method, omitUndefined({ deterministic, useContext }))
117112
// @ts-ignore
118-
this.fallback.addMethod(name, method, { deterministic, yields, useContext })
113+
this.fallback.addMethod(name, method, { deterministic, useContext })
119114
this.methods[name] = declareSync(method, sync)
120115
}
121116

122117
/**
123118
* Adds a batch of functions to the engine
124119
* @param {String} name
125120
* @param {Object} obj
126-
* @param {{ deterministic?: Boolean, yields?: Boolean, useContext?: Boolean, async?: Boolean, sync?: Boolean }} annotations Not recommended unless you're sure every function from the module will match these annotations.
121+
* @param {{ deterministic?: Boolean, useContext?: Boolean, async?: Boolean, sync?: Boolean }} annotations Not recommended unless you're sure every function from the module will match these annotations.
127122
*/
128123
addModule (name, obj, annotations = {}) {
129124
Object.getOwnPropertyNames(obj).forEach((key) => {
@@ -138,6 +133,14 @@ class AsyncLogicEngine {
138133
}
139134

140135
/**
136+
* Runs the logic against the data.
137+
*
138+
* NOTE: With interpreted optimizations enabled, it will cache the execution plan for the logic for
139+
* future invocations; if you plan to modify the logic, you should disable this feature, by passing
140+
* `disableInterpretedOptimization: true` in the constructor.
141+
*
142+
* If it detects that a bunch of dynamic objects are being passed in, and it doesn't see the same object,
143+
* it will disable the interpreted optimization.
141144
*
142145
* @param {*} logic The logic to be executed
143146
* @param {*} data The data being passed in to the logic to be executed against.
@@ -146,39 +149,35 @@ class AsyncLogicEngine {
146149
*/
147150
async run (logic, data = {}, options = {}) {
148151
const { above = [] } = options
152+
153+
// OPTIMIZER BLOCK //
154+
if (this.missesSinceSeen > 500) {
155+
this.disableInterpretedOptimization = true
156+
this.missesSinceSeen = 0
157+
}
158+
159+
if (!this.disableInterpretedOptimization && typeof logic === 'object' && logic && !this.optimizedMap.has(logic)) {
160+
this.optimizedMap.set(logic, optimize(logic, this, above))
161+
this.missesSinceSeen++
162+
return typeof this.optimizedMap.get(logic) === 'function' ? this.optimizedMap.get(logic)(data, above) : this.optimizedMap.get(logic)
163+
}
164+
165+
if (!this.disableInterpretedOptimization && logic && typeof logic === 'object' && this.optimizedMap.get(logic)) {
166+
this.missesSinceSeen = 0
167+
return typeof this.optimizedMap.get(logic) === 'function' ? this.optimizedMap.get(logic)(data, above) : this.optimizedMap.get(logic)
168+
}
169+
// END OPTIMIZER BLOCK //
170+
149171
if (Array.isArray(logic)) {
150172
const result = await Promise.all(
151173
logic.map((i) => this.run(i, data, { above }))
152174
)
153175

154-
if (this.options.yieldSupported && (await checkYield(result))) {
155-
return new EngineObject({
156-
result
157-
})
158-
}
159-
160176
return result
161177
}
162178

163179
if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) {
164-
const { func, result } = await this._parse(logic, data, above)
165-
166-
if (this.options.yieldSupported && (await checkYield(result))) {
167-
if (result instanceof YieldStructure) {
168-
if (result._input) {
169-
result._logic = { [func]: result._input }
170-
}
171-
if (!result._logic) {
172-
result._logic = logic
173-
}
174-
return result
175-
}
176-
177-
return new EngineObject({
178-
result: { [func]: result.data.result }
179-
})
180-
}
181-
180+
const { result } = await this._parse(logic, data, above)
182181
return result
183182
}
184183

async_optimizer.js

+78
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// This is the synchronous version of the optimizer; which the Async one should be based on.
2+
import { isDeterministic } from './compiler.js'
3+
import { map } from './async_iterators.js'
4+
5+
/**
6+
* Turns an expression like { '+': [1, 2] } into a function that can be called with data.
7+
* @param {*} logic
8+
* @param {*} engine
9+
* @param {string} methodName
10+
* @param {any[]} above
11+
* @returns A method that can be called to execute the logic.
12+
*/
13+
function getMethod (logic, engine, methodName, above) {
14+
const method = engine.methods[methodName]
15+
const called = method.asyncMethod ? method.asyncMethod : method.method ? method.method : method
16+
17+
if (method.traverse === false) {
18+
const args = logic[methodName]
19+
return (data, abv) => called(args, data, abv || above, engine)
20+
}
21+
22+
const args = logic[methodName]
23+
24+
if (Array.isArray(args)) {
25+
const optimizedArgs = args.map(l => optimize(l, engine, above))
26+
return async (data, abv) => {
27+
const evaluatedArgs = await map(optimizedArgs, l => typeof l === 'function' ? l(data, abv) : l)
28+
return called(evaluatedArgs, data, abv || above, engine)
29+
}
30+
} else {
31+
const optimizedArgs = optimize(args, engine, above)
32+
return async (data, abv) => {
33+
return called(typeof optimizedArgs === 'function' ? await optimizedArgs(data, abv) : optimizedArgs, data, abv || above, engine)
34+
}
35+
}
36+
}
37+
38+
/**
39+
* Processes the logic for the engine once so that it doesn't need to be traversed again.
40+
* @param {*} logic
41+
* @param {*} engine
42+
* @param {any[]} above
43+
* @returns A function that optimizes the logic for the engine in advance.
44+
*/
45+
export function optimize (logic, engine, above = []) {
46+
if (Array.isArray(logic)) {
47+
const arr = logic.map(l => optimize(l, engine, above))
48+
return async (data, abv) => map(arr, l => typeof l === 'function' ? l(data, abv) : l)
49+
};
50+
51+
if (logic && typeof logic === 'object') {
52+
const keys = Object.keys(logic)
53+
const methodName = keys[0]
54+
55+
const isData = engine.isData(logic, methodName)
56+
if (isData) return () => logic
57+
58+
// If we have a deterministic function, we can just return the result of the evaluation,
59+
// basically inlining the operation.
60+
const deterministic = !engine.disableInline && isDeterministic(logic, engine, { engine })
61+
62+
if (methodName in engine.methods) {
63+
const result = getMethod(logic, engine, methodName, above)
64+
if (deterministic) {
65+
let computed
66+
// For async, it's a little less straightforward since it could be a promise,
67+
// so we'll make it a closure.
68+
return async () => {
69+
if (!computed) computed = await result()
70+
return computed
71+
}
72+
}
73+
return result
74+
}
75+
}
76+
77+
return logic
78+
}

bench/test.js

+2-23
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { LogicEngine, AsyncLogicEngine } from '../index.js'
22
import fs from 'fs'
33
import { isDeepStrictEqual } from 'util'
4-
import traverseCopy from '../utilities/traverseCopy.js'
4+
// import traverseCopy from '../utilities/traverseCopy.js'
55
import jl from 'json-logic-js'
66

77
const x = new LogicEngine()
@@ -60,28 +60,7 @@ const defined = [
6060
[{ '*': [{ var: 'x' }, { var: 'y' }] }, { x: 1, y: 3 }, 3]
6161
]
6262
const tests = defined || compatible
63-
const other =
64-
tests ||
65-
traverseCopy(tests, [], {
66-
mutateKey: (i) => {
67-
if (i === 'map') {
68-
return 'mapYield'
69-
}
70-
if (i === 'reduce') {
71-
return 'reduceYield'
72-
}
73-
if (i === 'filter') {
74-
return 'filterYield'
75-
}
76-
if (i === 'every') {
77-
return 'everyYield'
78-
}
79-
if (i === 'some') {
80-
return 'someYield'
81-
}
82-
return i
83-
}
84-
})
63+
const other = tests
8564
const built = other.map((i) => {
8665
return x.build(i[0])
8766
})

0 commit comments

Comments
 (0)