Skip to content

Commit c4f4584

Browse files
committed
Improve compiler performance by reducing arguments passed into methods and removing unnecessary array allocations for "above" when it's not used.
1 parent 91584a7 commit c4f4584

File tree

4 files changed

+60
-15
lines changed

4 files changed

+60
-15
lines changed

compiler.js

+16-8
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ import {
66
Sync,
77
Compiled
88
} from './constants.js'
9-
import declareSync from './utilities/declareSync.js'
109

1110
// asyncIterators is required for the compiler to operate as intended.
1211
import asyncIterators from './async_iterators.js'
1312
import { coerceArray } from './utilities/coerceArray.js'
13+
import { countArguments } from './utilities/countArguments.js'
1414

1515
/**
1616
* Provides a simple way to compile logic into a function that can be run.
@@ -208,17 +208,22 @@ function buildString (method, buildState = {}) {
208208
if (!coerce && Array.isArray(lower) && lower.length === 1) lower = lower[0]
209209
else if (coerce && Array.isArray(lower)) coerce = ''
210210

211+
const argumentsDict = [', context', ', context, above', ', context, above, engine']
212+
211213
if (typeof engine.methods[func] === 'function') {
212214
asyncDetected = !isSync(engine.methods[func])
213-
return makeAsync(`engine.methods["${func}"](${coerce}(` + buildString(lower, buildState) + '), context, above, engine)')
215+
const argumentsNeeded = argumentsDict[countArguments(engine.methods[func]) - 1] || argumentsDict[2]
216+
return makeAsync(`engine.methods["${func}"](${coerce}(` + buildString(lower, buildState) + ')' + argumentsNeeded + ')')
214217
} else {
218+
asyncDetected = Boolean(async && engine.methods[func] && engine.methods[func].asyncMethod)
219+
const argCount = countArguments(asyncDetected ? engine.methods[func].asyncMethod : engine.methods[func].method)
220+
const argumentsNeeded = argumentsDict[argCount - 1] || argumentsDict[2]
221+
215222
if (engine.methods[func] && (typeof engine.methods[func].traverse === 'undefined' ? true : engine.methods[func].traverse)) {
216-
asyncDetected = Boolean(async && engine.methods[func] && engine.methods[func].asyncMethod)
217-
return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(${coerce}(` + buildString(lower, buildState) + '), context, above, engine)')
223+
return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(${coerce}(` + buildString(lower, buildState) + ')' + argumentsNeeded + ')')
218224
} else {
219-
asyncDetected = Boolean(async && engine.methods[func] && engine.methods[func].asyncMethod)
220225
notTraversed.push(lower)
221-
return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(` + `notTraversed[${notTraversed.length - 1}]` + ', context, above, engine)')
226+
return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(` + `notTraversed[${notTraversed.length - 1}]` + argumentsNeeded + ')')
222227
}
223228
}
224229
}
@@ -303,11 +308,14 @@ function processBuiltString (method, str, buildState) {
303308
})
304309

305310
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { const result = ${str}; return result }`
306-
307311
// console.log(str)
308312
// console.log(final)
309313
// eslint-disable-next-line no-eval
310-
return declareSync((typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray), !buildState.asyncDetected)
314+
return Object.assign(
315+
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray), {
316+
[Sync]: !buildState.asyncDetected,
317+
aboveDetected: str.includes(', above')
318+
})
311319
}
312320

313321
export { build }

defaultMethods.js

+11-6
Original file line numberDiff line numberDiff line change
@@ -310,28 +310,30 @@ const defaultMethods = {
310310
avoidInlineAsync: true
311311
}
312312
mapper = build(mapper, mapState)
313+
const aboveArray = mapper.aboveDetected ? '[null, context, above]' : 'null'
314+
313315
buildState.methods.push(mapper)
314316
if (async) {
315317
if (!isSync(mapper) || selector.includes('await')) {
316318
buildState.detectAsync = true
317319
if (typeof defaultValue !== 'undefined') {
318320
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
319321
buildState.methods.length - 1
320-
}]({ accumulator: a, current: b }, [null, context, above]), ${defaultValue})`
322+
}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})`
321323
}
322324
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
323325
buildState.methods.length - 1
324-
}]({ accumulator: a, current: b }, [null, context, above]))`
326+
}]({ accumulator: a, current: b }, ${aboveArray}))`
325327
}
326328
}
327329
if (typeof defaultValue !== 'undefined') {
328330
return `(${selector} || []).reduce((a,b) => methods[${
329331
buildState.methods.length - 1
330-
}]({ accumulator: a, current: b }, [null, context, above]), ${defaultValue})`
332+
}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})`
331333
}
332334
return `(${selector} || []).reduce((a,b) => methods[${
333335
buildState.methods.length - 1
334-
}]({ accumulator: a, current: b }, [null, context, above]))`
336+
}]({ accumulator: a, current: b }, ${aboveArray}))`
335337
},
336338
method: (input, context, above, engine) => {
337339
if (!Array.isArray(input)) throw new InvalidControlInput(input)
@@ -536,14 +538,17 @@ function createArrayIterativeMethod (name, useTruthy = false) {
536538
extraArguments: 'index, above'
537539
}
538540

541+
const method = build(mapper, mapState)
542+
const aboveArray = method.aboveDetected ? buildState.compile`[{ item: null }, context, above]` : buildState.compile`null`
543+
539544
if (async) {
540545
if (!isSyncDeep(mapper, buildState.engine, buildState)) {
541546
buildState.detectAsync = true
542-
return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x) => ${build(mapper, mapState)}(i, x, [{ item: null }, context, above]))`
547+
return buildState.compile`await asyncIterators[${name}](${selector} || [], async (i, x) => ${method}(i, x, ${aboveArray}))`
543548
}
544549
}
545550

546-
return buildState.compile`(${selector} || [])[${name}]((i, x) => ${build(mapper, mapState)}(i, x, [{ item: null }, context, above]))`
551+
return buildState.compile`(${selector} || [])[${name}]((i, x) => ${method}(i, x, ${aboveArray}))`
547552
},
548553
traverse: false
549554
}

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "json-logic-engine",
3-
"version": "4.0.0",
3+
"version": "4.0.1",
44
"description": "Construct complex rules with JSON & process them.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",

utilities/countArguments.js

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
const counts = new WeakMap()
2+
3+
/**
4+
* Counts the number of arguments a function has; paying attention to the function's signature
5+
* to avoid edge cases.
6+
* This is used to allow for compiler optimizations.
7+
* @param {(...args: any[]) => any} fn
8+
* @returns {number}
9+
*/
10+
export function countArguments (fn) {
11+
if (!fn || typeof fn !== 'function' || !fn.length) return 0
12+
if (!counts.has(fn)) counts.set(fn, _countArguments(fn))
13+
return counts.get(fn)
14+
}
15+
16+
/**
17+
* Counts the number of arguments a function has; paying attention to the function's signature.
18+
* This is the internal implementation that does not use a WeakMap.
19+
* @param {(...args: any[]) => any} fn
20+
* @returns {number}
21+
*/
22+
function _countArguments (fn) {
23+
if (!fn || typeof fn !== 'function' || !fn.length) return 0
24+
let fnStr = fn.toString()
25+
if (fnStr[0] !== '(' && fnStr[0] !== 'f') return 0
26+
fnStr = fnStr.substring(fnStr.indexOf('('), fnStr.indexOf('{')).replace(/=>/g, '')
27+
28+
// regex to check for "..." or "="
29+
const regex = /\.{3}|=/
30+
if (regex.test(fnStr)) return 0
31+
return fn.length
32+
}

0 commit comments

Comments
 (0)