Skip to content

Commit 3b06d51

Browse files
committed
Disable object accumulators by default; still trying to figure out how I
want to handle the max length thing efficiently...
1 parent 2d12635 commit 3b06d51

File tree

7 files changed

+88
-53
lines changed

7 files changed

+88
-53
lines changed

asyncLogic.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,15 @@ class AsyncLogicEngine {
2323
* - In mainline: empty arrays are falsey; in our implementation, they are truthy.
2424
*
2525
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
26-
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: boolean, permissive?: boolean }} options
26+
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: boolean, permissive?: boolean, enableObjectAccumulators?: boolean, maxArrayLength?: number, maxStringLength?: number }} options
2727
*/
2828
constructor (
2929
methods = defaultMethods,
30-
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false }
30+
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false, enableObjectAccumulators: false, maxArrayLength: 1 << 15, maxStringLength: 1 << 16 }
3131
) {
3232
this.methods = { ...methods }
33-
/** @type {{disableInline?: Boolean, disableInterpretedOptimization?: Boolean }} */
34-
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization }
33+
/** @type {{disableInline?: Boolean, disableInterpretedOptimization?: Boolean, enableObjectAccumulators?: Boolean, maxArrayLength?: number, maxStringLength?: number}} */
34+
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization, enableObjectAccumulators: options.enableObjectAccumulators || false, maxArrayLength: options.maxArrayLength || (1 << 15), maxStringLength: options.maxStringLength || (1 << 16) }
3535
this.disableInline = options.disableInline
3636
this.disableInterpretedOptimization = options.disableInterpretedOptimization
3737
this.async = true

async_iterators.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
// @ts-check
22
'use strict'
3+
4+
import { assertNotType } from './utilities/downgrade.js'
5+
36
// Note: Each of these iterators executes synchronously, and will not "run in parallel"
47
// I am supporting filter, reduce, some, every, map
58
export async function filter (arr, iter) {
@@ -36,19 +39,17 @@ export async function map (arr, iter) {
3639
return result
3740
}
3841

39-
export async function reduce (arr, iter, defaultValue) {
42+
export async function reduce (arr, iter, defaultValue, skipTypeCheck = false) {
4043
if (arr.length === 0) {
41-
if (typeof defaultValue !== 'undefined') {
42-
return defaultValue
43-
}
44+
if (typeof defaultValue !== 'undefined') return defaultValue
4445
throw new Error('Array has no elements.')
4546
}
4647

4748
const start = typeof defaultValue === 'undefined' ? 1 : 0
48-
let data = start ? arr[0] : defaultValue
49+
let data = assertNotType(start ? arr[0] : defaultValue, skipTypeCheck ? '' : 'object')
4950

5051
for (let i = start; i < arr.length; i++) {
51-
data = await iter(data, arr[i])
52+
data = assertNotType(await iter(data, arr[i]), skipTypeCheck ? '' : 'object')
5253
}
5354

5455
return data

compiler.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
import asyncIterators from './async_iterators.js'
1212
import { coerceArray } from './utilities/coerceArray.js'
1313
import { countArguments } from './utilities/countArguments.js'
14-
import { precoerceNumber, assertSize, compareCheck } from './utilities/downgrade.js'
14+
import { precoerceNumber, assertSize, compareCheck, assertNotType } from './utilities/downgrade.js'
1515

1616
/**
1717
* Provides a simple way to compile logic into a function that can be run.
@@ -319,12 +319,12 @@ function processBuiltString (method, str, buildState) {
319319
str = str.replace(`__%%%${x}%%%__`, item)
320320
})
321321

322-
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }`
322+
const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck, assertNotType) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }`
323323
// console.log(str)
324324
// console.log(final)
325325
// eslint-disable-next-line no-eval
326326
return Object.assign(
327-
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck), {
327+
(typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber, assertSize, compareCheck, assertNotType), {
328328
[Sync]: !buildState.asyncDetected,
329329
deterministic: !str.includes('('),
330330
aboveDetected: typeof str === 'string' && str.includes(', above')

defaultMethods.js

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import declareSync from './utilities/declareSync.js'
77
import { build, buildString } from './compiler.js'
88
import chainingSupported from './utilities/chainingSupported.js'
99
import legacyMethods from './legacy.js'
10-
import { precoerceNumber } from './utilities/downgrade.js'
10+
import { precoerceNumber, assertNotType } from './utilities/downgrade.js'
1111

1212
const INVALID_ARGUMENTS = { type: 'Invalid Arguments' }
1313

@@ -32,7 +32,7 @@ function isDeterministic (method, engine, buildState) {
3232
return typeof engine.methods[func].deterministic === 'function'
3333
? engine.methods[func].deterministic(lower, buildState)
3434
: engine.methods[func].deterministic &&
35-
isDeterministic(lower, engine, buildState)
35+
isDeterministic(lower, engine, buildState)
3636
}
3737
return true
3838
}
@@ -315,9 +315,9 @@ const defaultMethods = {
315315
if (Array.isArray(data) && data.length) {
316316
return `(${data.map((i, x) => {
317317
const built = buildString(i, buildState)
318-
if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built
319-
return '(' + built + ')'
320-
}).join(' ?? ')})`
318+
if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built
319+
return '(' + built + ')'
320+
}).join(' ?? ')})`
321321
}
322322
return `(${buildString(data, buildState)}).reduce((a,b) => (a) ?? b, null)`
323323
},
@@ -653,40 +653,37 @@ const defaultMethods = {
653653
}
654654
mapper = build(mapper, mapState)
655655
const aboveArray = mapper.aboveDetected ? '[null, context, above]' : 'null'
656-
656+
const verifyAccumulator = buildState.engine.options.enableObjectAccumulators ? '' : 'assertNotType'
657657
buildState.methods.push(mapper)
658+
658659
if (async) {
659660
if (!isSync(mapper) || selector.includes('await')) {
660661
buildState.asyncDetected = true
661662
if (typeof defaultValue !== 'undefined') {
662-
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
663-
buildState.methods.length - 1
664-
}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})`
663+
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue}, ${buildState.engine.options.enableObjectAccumulators})`
665664
}
666-
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${
667-
buildState.methods.length - 1
668-
}]({ accumulator: a, current: b }, ${aboveArray}))`
665+
return `await asyncIterators.reduce(${selector} || [], (a,b) => methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray}), undefined, ${buildState.engine.options.enableObjectAccumulators})`
669666
}
670667
}
668+
671669
if (typeof defaultValue !== 'undefined') {
672-
return `(${selector} || []).reduce((a,b) => methods[${
673-
buildState.methods.length - 1
674-
}]({ accumulator: a, current: b }, ${aboveArray}), ${defaultValue})`
670+
return `(${selector} || []).reduce((a,b) => ${verifyAccumulator}(methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray})), ${verifyAccumulator}(${defaultValue}))`
675671
}
676-
return `(${selector} || []).reduce((a,b) => methods[${
677-
buildState.methods.length - 1
678-
}]({ accumulator: a, current: b }, ${aboveArray}))`
672+
return `(${selector} || []).reduce((a,b) => ${verifyAccumulator}(methods[${buildState.methods.length - 1}]({ accumulator: a, current: b }, ${aboveArray})))`
679673
},
680674
method: (input, context, above, engine) => {
681675
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
682676
let [selector, mapper, defaultValue] = input
683-
defaultValue = runOptimizedOrFallback(defaultValue, engine, context, above)
677+
678+
const verifyAccumulator = engine.options.enableObjectAccumulators ? a => a : assertNotType
679+
680+
defaultValue = verifyAccumulator(runOptimizedOrFallback(defaultValue, engine, context, above))
684681
selector = runOptimizedOrFallback(selector, engine, context, above) || []
685-
let func = (accumulator, current) => engine.run(mapper, { accumulator, current }, { above: [selector, context, above] })
682+
let func = (accumulator, current) => verifyAccumulator(engine.run(mapper, { accumulator, current }, { above: [selector, context, above] }), 'object')
686683

687684
if (engine.optimizedMap.has(mapper) && typeof engine.optimizedMap.get(mapper) === 'function') {
688685
const optimized = engine.optimizedMap.get(mapper)
689-
func = (accumulator, current) => optimized({ accumulator, current }, [selector, context, above])
686+
func = (accumulator, current) => verifyAccumulator(optimized({ accumulator, current }, [selector, context, above]))
690687
}
691688

692689
if (typeof defaultValue === 'undefined') return selector.reduce(func)
@@ -697,13 +694,10 @@ const defaultMethods = {
697694
asyncMethod: async (input, context, above, engine) => {
698695
if (!Array.isArray(input)) throw INVALID_ARGUMENTS
699696
let [selector, mapper, defaultValue] = input
700-
defaultValue = await engine.run(defaultValue, context, {
701-
above
702-
})
703-
selector =
704-
(await engine.run(selector, context, {
705-
above
706-
})) || []
697+
const verifyAccumulator = engine.options.enableObjectAccumulators ? a => a : assertNotType
698+
699+
defaultValue = verifyAccumulator(await engine.run(defaultValue, context, { above }))
700+
selector = (await engine.run(selector, context, { above })) || []
707701
return asyncIterators.reduce(
708702
selector,
709703
(accumulator, current) => {
@@ -718,7 +712,8 @@ const defaultMethods = {
718712
}
719713
)
720714
},
721-
defaultValue
715+
defaultValue,
716+
engine.enableObjectAccumulators
722717
)
723718
},
724719
lazy: true
@@ -827,7 +822,8 @@ const defaultMethods = {
827822
})
828823
return accumulator
829824
},
830-
{}
825+
{},
826+
true
831827
)
832828
return result
833829
}
@@ -1058,11 +1054,11 @@ defaultMethods['/'].compile = function (data, buildState) {
10581054
if (data.length === 0) throw INVALID_ARGUMENTS
10591055
if (data.length === 1) data = [1, data[0]]
10601056
return `precoerceNumber(${data.map((i, x) => {
1061-
let res = numberCoercion(i, buildState)
1062-
if (x && res === '+0') precoerceNumber(NaN)
1063-
if (x) res = `precoerceNumber(${res} || NaN)`
1064-
return res
1065-
}).join(' / ')})`
1057+
let res = numberCoercion(i, buildState)
1058+
if (x && res === '+0') precoerceNumber(NaN)
1059+
if (x) res = `precoerceNumber(${res} || NaN)`
1060+
return res
1061+
}).join(' / ')})`
10661062
}
10671063
return `assertSize(prev = ${buildString(data, buildState)}, 1) && prev.length === 1 ? 1 / precoerceNumber(prev[0] || NaN) : prev.reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b || NaN)))`
10681064
}

logic.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ class LogicEngine {
1818
* Creates a new instance of the Logic Engine.
1919
*
2020
* @param {Object} methods An object that stores key-value pairs between the names of the commands & the functions they execute.
21-
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, permissive?: boolean }} options
21+
* @param {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, permissive?: boolean, enableObjectAccumulators?: boolean, maxArrayLength?: number, maxStringLength?: number }} options
2222
*/
2323
constructor (
2424
methods = defaultMethods,
25-
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false }
25+
options = { disableInline: false, disableInterpretedOptimization: false, permissive: false, enableObjectAccumulators: false, maxArrayLength: 1 << 15, maxStringLength: 1 << 16 }
2626
) {
2727
this.disableInline = options.disableInline
2828
this.disableInterpretedOptimization = options.disableInterpretedOptimization
@@ -31,8 +31,8 @@ class LogicEngine {
3131
this.optimizedMap = new WeakMap()
3232
this.missesSinceSeen = 0
3333

34-
/** @type {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean }} */
35-
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization }
34+
/** @type {{ disableInline?: Boolean, disableInterpretedOptimization?: Boolean, enableObjectAccumulators?: boolean, maxArrayLength?: number, maxStringLength?: number }} */
35+
this.options = { disableInline: options.disableInline, disableInterpretedOptimization: options.disableInterpretedOptimization, enableObjectAccumulators: options.enableObjectAccumulators || false, maxArrayLength: options.maxArrayLength || (1 << 15), maxStringLength: options.maxStringLength || (1 << 16) }
3636
if (!this.isData) {
3737
if (!options.permissive) this.isData = () => false
3838
else this.isData = (data, key) => !(key in this.methods)

suites/additional.json

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,29 @@
7676
"rule": { "val": { "cat": ["te", "st"] } },
7777
"data": { "test": 1 },
7878
"result": 1
79+
},
80+
{
81+
"description": "Throws when you try to use reduce with an array as the accumulator (Default)",
82+
"rule": { "reduce": [[1,2,3], 0, []] },
83+
"error": { "type": "Invalid Return" },
84+
"data": null
85+
},
86+
{
87+
"description": "Throws when you try to use reduce with an array as the accumulator (Return)",
88+
"rule": { "reduce": [[1,2,3], []] },
89+
"error": { "type": "Invalid Return" },
90+
"data": null
91+
},
92+
{
93+
"description": "Throws when you try to use reduce with an object as the accumulator (Default)",
94+
"rule": { "reduce": [[1,2,3], 0, {}] },
95+
"error": { "type": "Invalid Return" },
96+
"data": null
97+
},
98+
{
99+
"description": "Throws when you try to use reduce with an object as the accumulator (Return)",
100+
"rule": { "reduce": [[1,2,3], {}] },
101+
"error": { "type": "Invalid Return" },
102+
"data": null
79103
}
80-
]
104+
]

utilities/downgrade.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ export function assertSize (arr, size) {
2020
return arr
2121
}
2222

23+
/**
24+
* Asserts that an item is not of a certain type.
25+
* Treats null as its own type, "null".
26+
* The main use-case for this is to prevent reduce from returning objects.
27+
* @param {*} item
28+
* @param {string} type
29+
*/
30+
export function assertNotType (item, type = 'object', error = 'Invalid Return') {
31+
const typeOfItem = item === null ? 'null' : typeof item
32+
// eslint-disable-next-line no-throw-literal
33+
if (typeOfItem === type) throw { type: error }
34+
return item
35+
}
36+
2337
/**
2438
* Used to assert in compiled templates that when a numeric comparison is made, both values are numbers.
2539
* @param {*} item

0 commit comments

Comments
 (0)