From 3c6d8fa7e5ec0bc4988c61ed8308baef481dabba Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 01:09:50 -0600 Subject: [PATCH 01/15] Experimenting with Soft Errors --- compatibility.js | 1 + compatible.test.js | 1 + defaultMethods.js | 24 +++++++++++++++++++----- suites/divide.json | 18 ++++++++++++++++++ suites/errors.json | 15 +++++++++++++++ suites/minus.json | 12 ++++++++++++ suites/modulo.json | 12 ++++++++++++ suites/multiply.json | 12 ++++++++++++ suites/plus.json | 22 ++++++++++++++++++++-- 9 files changed, 110 insertions(+), 7 deletions(-) create mode 100644 suites/errors.json diff --git a/compatibility.js b/compatibility.js index 6bc75de..acacec0 100644 --- a/compatibility.js +++ b/compatibility.js @@ -24,6 +24,7 @@ const all = { function truthy (value) { if (Array.isArray(value) && value.length === 0) return false + if (Number.isNaN(value)) return true return value } diff --git a/compatible.test.js b/compatible.test.js index 141a9f3..107b320 100644 --- a/compatible.test.js +++ b/compatible.test.js @@ -16,6 +16,7 @@ for (const file of files) { function correction (x) { // eslint-disable-next-line no-compare-neg-zero if (x === -0) return 0 + if (Number.isNaN(x)) return { error: 'NaN' } return x } diff --git a/defaultMethods.js b/defaultMethods.js index a3800ad..b84d07c 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -56,8 +56,12 @@ const defaultMethods = { if (typeof data === 'string') return +data if (typeof data === 'number') return +data if (typeof data === 'boolean') return +data + if (typeof data === 'object' && !Array.isArray(data)) return Number.NaN let res = 0 - for (let i = 0; i < data.length; i++) res += +data[i] + for (let i = 0; i < data.length; i++) { + if (data[i] && typeof data[i] === 'object') return Number.NaN + res += +data[i] + } return res }, '*': (data) => { @@ -85,6 +89,11 @@ const defaultMethods = { for (let i = 1; i < data.length; i++) res %= +data[i] return res }, + error: (type) => { + if (Array.isArray(type)) type = type[0] + if (type === 'NaN') return Number.NaN + return { error: type } + }, max: (data) => Math.max(...data), min: (data) => Math.min(...data), in: ([item, array]) => (array || []).includes(item), @@ -855,9 +864,14 @@ defaultMethods['==='].compile = function (data, buildState) { defaultMethods['+'].compile = function (data, buildState) { if (Array.isArray(data)) { return `(${data - .map((i) => `(+${buildString(i, buildState)})`) + .map((i) => { + // Todo: Actually make this correct, this is a decent optimization but + // does not coerce the built string. + if (Array.isArray(i)) return 'NaN' + return `(+${buildString(i, buildState)})` + }) .join(' + ')})` - } else if (typeof data === 'string' || typeof data === 'number') { + } else if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { return `(+${buildString(data, buildState)})` } else { return `([].concat(${buildString( @@ -943,12 +957,12 @@ defaultMethods.not = defaultMethods['!'] // @ts-ignore Allow custom attribute defaultMethods['!!'].compile = function (data, buildState) { if (Array.isArray(data)) return buildState.compile`(!!engine.truthy(${data[0]}))` - return `(!!engine.truthy(${data}))` + return buildState.compile`(!!engine.truthy(${data}))` } defaultMethods.none.deterministic = defaultMethods.some.deterministic // @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations -defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = true +defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = true export default { ...defaultMethods, diff --git a/suites/divide.json b/suites/divide.json index 3685034..6dcaa60 100644 --- a/suites/divide.json +++ b/suites/divide.json @@ -146,5 +146,23 @@ "rule": { "/": [{ "val": "x" }, { "val": "y" }] }, "data": { "x": 8, "y": 2 }, "result": 4 + }, + { + "description": "Divide by Zero", + "rule": { "/": [0, 0] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Divide by NaN", + "rule": { "/": [1, { "error": "NaN" }] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Divide with String produces NaN", + "rule": { "/": [1, "a"] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/errors.json b/suites/errors.json new file mode 100644 index 0000000..715d887 --- /dev/null +++ b/suites/errors.json @@ -0,0 +1,15 @@ +[ + "# Some error tests", + { + "description": "NaN is Truthy", + "rule": { "!!": { "error": "NaN" } }, + "result": true, + "data": null + }, + { + "description": "Arbitrary error is Truthy", + "rule": { "!!": { "error": "Some error" } }, + "result": true, + "data": null + } +] \ No newline at end of file diff --git a/suites/minus.json b/suites/minus.json index 480a8af..ae4b404 100644 --- a/suites/minus.json +++ b/suites/minus.json @@ -114,5 +114,17 @@ "rule": { "-": [{ "val": "x" }, { "val": "y" }] }, "data": { "x": 1, "y": 2 }, "result": -1 + }, + { + "description": "Subtraction with NaN", + "rule": { "-": [{ "error": "NaN" }, 1] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Subtraction with string produces NaN", + "rule": { "-": ["Hey", 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/modulo.json b/suites/modulo.json index 45e75ec..261c34d 100644 --- a/suites/modulo.json +++ b/suites/modulo.json @@ -157,5 +157,17 @@ "rule": { "%": [{ "val": "x" }, { "val": "y" }] }, "data": { "x": 11, "y": 6 }, "result": 5 + }, + { + "description": "Modulo with NaN", + "rule": { "%": [{ "error": "NaN" }, 1] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Modulo with string produces NaN", + "rule": { "%": ["Hey", 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/multiply.json b/suites/multiply.json index 4e8e829..4f354df 100644 --- a/suites/multiply.json +++ b/suites/multiply.json @@ -144,5 +144,17 @@ "rule": { "*": [{ "val": "x" }, { "val": "y" }] }, "data": { "x": 8, "y": 2 }, "result": 16 + }, + { + "description": "Multiply with NaN", + "rule": { "*": [{ "error": "NaN" }, 1] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Multiply with string produces NaN", + "rule": { "*": ["Hey", 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/plus.json b/suites/plus.json index 82aa130..7abc5d6 100644 --- a/suites/plus.json +++ b/suites/plus.json @@ -138,7 +138,25 @@ { "description": "Addition with val", "rule": { "+": [{ "val": "x" }, { "val": "y" }] }, - "data": { "x": 1, "y": 2 }, - "result": 3 + "result": 3, + "data": { "x": 1, "y": 2 } + }, + { + "description": "Addition with NaN", + "rule": { "+": [{ "error": "NaN" }, 1] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Addition with string produces NaN", + "rule": { "+": ["Hey", 1] }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Addition with Array produces NaN", + "rule": { "+": [[1], 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file From 5d5c38d079c4c793c53eeb88759206277a368e7a Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 01:29:49 -0600 Subject: [PATCH 02/15] Add another check, and throws --- defaultMethods.js | 8 +++++++- suites/plus.json | 6 ++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/defaultMethods.js b/defaultMethods.js index b84d07c..e81883a 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -94,6 +94,12 @@ const defaultMethods = { if (type === 'NaN') return Number.NaN return { error: type } }, + throws: (item) => { + if (Array.isArray(item)) item = item[0] + if (Number.isNaN(item)) throw new Error('NaN was returned from expression') + if (item && item.error) throw item.error + return item + }, max: (data) => Math.max(...data), min: (data) => Math.min(...data), in: ([item, array]) => (array || []).includes(item), @@ -962,7 +968,7 @@ defaultMethods['!!'].compile = function (data, buildState) { defaultMethods.none.deterministic = defaultMethods.some.deterministic // @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations -defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = true +defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = defaultMethods.throws.optimizeUnary = true export default { ...defaultMethods, diff --git a/suites/plus.json b/suites/plus.json index 7abc5d6..9431e7c 100644 --- a/suites/plus.json +++ b/suites/plus.json @@ -158,5 +158,11 @@ "rule": { "+": [[1], 1] }, "result": { "error": "NaN" }, "data": null + }, + { + "description": "Addition with Object produces NaN", + "rule": { "+": [{ "val": "x" }, 1] }, + "result": { "error": "NaN" }, + "data": { "x": {} } } ] \ No newline at end of file From f33b29d12c5bdd69f4c1b14d8c8127fa7a5bbad7 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 01:37:55 -0600 Subject: [PATCH 03/15] Incorporate more of the error boundaries into the numeric operators (interpreted) --- defaultMethods.js | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index e81883a..fe632e6 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -66,12 +66,19 @@ const defaultMethods = { }, '*': (data) => { let res = 1 - for (let i = 0; i < data.length; i++) res *= +data[i] + for (let i = 0; i < data.length; i++) { + if (data[i] && typeof data[i] === 'object') return Number.NaN + res *= +data[i] + } return res }, '/': (data) => { + if (data[0] && typeof data[0] === 'object') return Number.NaN let res = +data[0] - for (let i = 1; i < data.length; i++) res /= +data[i] + for (let i = 1; i < data.length; i++) { + if (data[i] && typeof data[i] === 'object') return Number.NaN + res /= +data[i] + } return res }, '-': (data) => { @@ -79,14 +86,22 @@ const defaultMethods = { if (typeof data === 'string') return -data if (typeof data === 'number') return -data if (typeof data === 'boolean') return -data + if (typeof data === 'object' && !Array.isArray(data)) return Number.NaN if (data.length === 1) return -data[0] let res = data[0] - for (let i = 1; i < data.length; i++) res -= +data[i] + for (let i = 1; i < data.length; i++) { + if (data[i] && typeof data[i] === 'object') return Number.NaN + res -= +data[i] + } return res }, '%': (data) => { + if (data[0] && typeof data[0] === 'object') return Number.NaN let res = +data[0] - for (let i = 1; i < data.length; i++) res %= +data[i] + for (let i = 1; i < data.length; i++) { + if (data[i] && typeof data[i] === 'object') return Number.NaN + res %= +data[i] + } return res }, error: (type) => { From 4686e90a2e23b81fa52b9c5b5fe66dc7881a0aaa Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 10:18:43 -0600 Subject: [PATCH 04/15] Adds error coalescing --- compiler.js | 7 ++++--- defaultMethods.js | 14 ++++++++++---- suites/errors.json | 12 ++++++++++++ utilities/downgrade.js | 10 ++++++++++ 4 files changed, 36 insertions(+), 7 deletions(-) create mode 100644 utilities/downgrade.js diff --git a/compiler.js b/compiler.js index 9fad6ec..3b1ad9a 100644 --- a/compiler.js +++ b/compiler.js @@ -11,6 +11,7 @@ import { import asyncIterators from './async_iterators.js' import { coerceArray } from './utilities/coerceArray.js' import { countArguments } from './utilities/countArguments.js' +import { downgrade } from './utilities/downgrade.js' /** * Provides a simple way to compile logic into a function that can be run. @@ -307,12 +308,12 @@ function processBuiltString (method, str, buildState) { str = str.replace(`__%%%${x}%%%__`, item) }) - const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { let prev; const result = ${str}; return result }` + const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { let prev; const result = ${str}; return result }` // console.log(str) - // console.log(final) + console.log(final) // eslint-disable-next-line no-eval return Object.assign( - (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray), { + (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade), { [Sync]: !buildState.asyncDetected, aboveDetected: typeof str === 'string' && str.includes(', above') }) diff --git a/defaultMethods.js b/defaultMethods.js index fe632e6..93d627b 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -8,6 +8,7 @@ import { build, buildString } from './compiler.js' import chainingSupported from './utilities/chainingSupported.js' import InvalidControlInput from './errors/InvalidControlInput.js' import legacyMethods from './legacy.js' +import { downgrade } from './utilities/downgrade.js' function isDeterministic (method, engine, buildState) { if (Array.isArray(method)) { @@ -255,7 +256,7 @@ const defaultMethods = { let item for (let i = 0; i < arr.length; i++) { item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] - if (item !== null && item !== undefined) return item + if (downgrade(item) !== null && item !== undefined) return item } if (item === undefined) return null @@ -269,7 +270,7 @@ const defaultMethods = { let item for (let i = 0; i < arr.length; i++) { item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] - if (item !== null && item !== undefined) return item + if (downgrade(item) !== null && item !== undefined) return item } if (item === undefined) return null @@ -278,8 +279,13 @@ const defaultMethods = { deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { if (!chainingSupported) return false - if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' ?? ')})` - return `(${buildString(data, buildState)}).reduce((a,b) => a ?? b, null)` + if (Array.isArray(data) && data.length) { + return `(${data.map((i, x) => { + if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return buildString(i, buildState) + return 'downgrade(' + buildString(i, buildState) + ')' + }).join(' ?? ')})` + } + return `(${buildString(data, buildState)}).reduce((a,b) => downgrade(a) ?? b, null)` }, traverse: false }, diff --git a/suites/errors.json b/suites/errors.json index 715d887..9aca656 100644 --- a/suites/errors.json +++ b/suites/errors.json @@ -11,5 +11,17 @@ "rule": { "!!": { "error": "Some error" } }, "result": true, "data": null + }, + { + "description": "Coalesce an error", + "rule": { "??": [{ "error": "Some error" }, 1] }, + "result": 1, + "data": null + }, + { + "description": "Coalesce an emitted error", + "rule": { "??": [{ "+": [{ "val": "hello" }]}, 1] }, + "result": 1, + "data": { "hello": "world" } } ] \ No newline at end of file diff --git a/utilities/downgrade.js b/utilities/downgrade.js new file mode 100644 index 0000000..5940bf3 --- /dev/null +++ b/utilities/downgrade.js @@ -0,0 +1,10 @@ + +/** + * Used to make an "error" piece of data null, for the purposes of coalescing. + * @param {any} item + */ +export function downgrade (item) { + if (item && typeof item === 'object' && 'error' in item) return null + if (Number.isNaN(item)) return null + return item +} From 071906efe0dbabde200b6344cf348da3861d4f65 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 10:20:33 -0600 Subject: [PATCH 05/15] Comment the final ine back, --- compiler.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/compiler.js b/compiler.js index 3b1ad9a..c0f10de 100644 --- a/compiler.js +++ b/compiler.js @@ -310,7 +310,7 @@ function processBuiltString (method, str, buildState) { const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { let prev; const result = ${str}; return result }` // console.log(str) - console.log(final) + // console.log(final) // eslint-disable-next-line no-eval return Object.assign( (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade), { From 647a97702bcd3229f78e4e03b77ca46f31464cff Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 10:45:32 -0600 Subject: [PATCH 06/15] Set up NaN coercion properly --- compiler.js | 8 +++--- defaultMethods.js | 63 ++++++++++++++---------------------------- suites/plus.json | 6 ++++ utilities/downgrade.js | 10 +++++++ 4 files changed, 40 insertions(+), 47 deletions(-) diff --git a/compiler.js b/compiler.js index c0f10de..31b4280 100644 --- a/compiler.js +++ b/compiler.js @@ -11,7 +11,7 @@ import { import asyncIterators from './async_iterators.js' import { coerceArray } from './utilities/coerceArray.js' import { countArguments } from './utilities/countArguments.js' -import { downgrade } from './utilities/downgrade.js' +import { downgrade, precoerceNumber } from './utilities/downgrade.js' /** * Provides a simple way to compile logic into a function that can be run. @@ -194,7 +194,7 @@ function buildString (method, buildState = {}) { } let lower = method[func] - if (!lower || typeof lower !== 'object') lower = [lower] + if ((!lower || typeof lower !== 'object') && (typeof engine.methods[func].traverse === 'undefined' || engine.methods[func].traverse)) lower = [lower] if (engine.methods[func] && engine.methods[func].compile) { let str = engine.methods[func].compile(lower, buildState) @@ -308,12 +308,12 @@ function processBuiltString (method, str, buildState) { str = str.replace(`__%%%${x}%%%__`, item) }) - const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { let prev; const result = ${str}; return result }` + const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { let prev; const result = ${str}; return result }` // console.log(str) // console.log(final) // eslint-disable-next-line no-eval return Object.assign( - (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade), { + (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber), { [Sync]: !buildState.asyncDetected, aboveDetected: typeof str === 'string' && str.includes(', above') }) diff --git a/defaultMethods.js b/defaultMethods.js index 93d627b..25a65ae 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -887,36 +887,27 @@ defaultMethods['==='].compile = function (data, buildState) { for (let i = 2; i < data.length; i++) res = buildState.compile`(${res} && ${data[i - 1]} === ${data[i]})` return res } + +/** + * Transforms the operands of the arithmetic operation to numbers. + */ +function numberCoercion (i, buildState) { + if (Array.isArray(i)) return 'NaN' + if (typeof i === 'string' || typeof i === 'number' || typeof i === 'boolean') return `(+${buildString(i, buildState)})` + return `(+precoerceNumber(${buildString(i, buildState)}))` +} + // @ts-ignore Allow custom attribute defaultMethods['+'].compile = function (data, buildState) { - if (Array.isArray(data)) { - return `(${data - .map((i) => { - // Todo: Actually make this correct, this is a decent optimization but - // does not coerce the built string. - if (Array.isArray(i)) return 'NaN' - return `(+${buildString(i, buildState)})` - }) - .join(' + ')})` - } else if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') { - return `(+${buildString(data, buildState)})` - } else { - return `([].concat(${buildString( - data, - buildState - )})).reduce((a,b) => (+a)+(+b), 0)` - } + if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' + ')})` + if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `(+${buildString(data, buildState)})` + return `([].concat(${buildString(data, buildState)})).reduce((a,b) => (+a)+(+precoerceNumber(b)), 0)` } // @ts-ignore Allow custom attribute defaultMethods['%'].compile = function (data, buildState) { - if (Array.isArray(data)) { - return `(${data - .map((i) => `(+${buildString(i, buildState)})`) - .join(' % ')})` - } else { - return `(${buildString(data, buildState)}).reduce((a,b) => (+a)%(+b))` - } + if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' % ')})` + return `(${buildString(data, buildState)}).reduce((a,b) => (+a)%(+precoerceNumber(b)))` } // @ts-ignore Allow custom attribute @@ -927,11 +918,7 @@ defaultMethods.in.compile = function (data, buildState) { // @ts-ignore Allow custom attribute defaultMethods['-'].compile = function (data, buildState) { - if (Array.isArray(data)) { - return `${data.length === 1 ? '-' : ''}(${data - .map((i) => `(+${buildString(i, buildState)})`) - .join(' - ')})` - } + if (Array.isArray(data)) return `${data.length === 1 ? '-' : ''}(${data.map(i => numberCoercion(i, buildState)).join(' - ')})` if (typeof data === 'string' || typeof data === 'number') { return `(-${buildString(data, buildState)})` } else { @@ -943,23 +930,13 @@ defaultMethods['-'].compile = function (data, buildState) { } // @ts-ignore Allow custom attribute defaultMethods['/'].compile = function (data, buildState) { - if (Array.isArray(data)) { - return `(${data - .map((i) => `(+${buildString(i, buildState)})`) - .join(' / ')})` - } else { - return `(${buildString(data, buildState)}).reduce((a,b) => (+a)/(+b))` - } + if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' / ')})` + return `(${buildString(data, buildState)}).reduce((a,b) => (+a)/(+b))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) { - if (Array.isArray(data)) { - return `(${data - .map((i) => `(+${buildString(i, buildState)})`) - .join(' * ')})` - } else { - return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))` - } + if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' * ')})` + return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))` } // @ts-ignore Allow custom attribute defaultMethods.cat.compile = function (data, buildState) { diff --git a/suites/plus.json b/suites/plus.json index 9431e7c..0350fd1 100644 --- a/suites/plus.json +++ b/suites/plus.json @@ -159,6 +159,12 @@ "result": { "error": "NaN" }, "data": null }, + { + "description": "Addition with Array from context produces NaN", + "rule": { "+": [{ "val": "x" }, 1] }, + "result": { "error": "NaN" }, + "data": { "x": [1] } + }, { "description": "Addition with Object produces NaN", "rule": { "+": [{ "val": "x" }, 1] }, diff --git a/utilities/downgrade.js b/utilities/downgrade.js index 5940bf3..9fb056e 100644 --- a/utilities/downgrade.js +++ b/utilities/downgrade.js @@ -8,3 +8,13 @@ export function downgrade (item) { if (Number.isNaN(item)) return null return item } + +/** + * Used to precoerce a data value to a number, for the purposes of coalescing. + * @param {any} item + */ +export function precoerceNumber (item) { + if (!item) return item + if (typeof item === 'object') return Number.isNaN + return item +} From 3081e73f76e2b2a6acd047aa86e51a9bdfdb7450 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 11:40:26 -0600 Subject: [PATCH 07/15] Add additional error boundaries --- defaultMethods.js | 1 + suites/divide.json | 6 ++++++ suites/errors.json | 6 ++++++ suites/minus.json | 6 ++++++ suites/modulo.json | 6 ++++++ suites/multiply.json | 6 ++++++ suites/plus.json | 12 ++++++++++++ 7 files changed, 43 insertions(+) diff --git a/defaultMethods.js b/defaultMethods.js index 25a65ae..f252fcb 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -88,6 +88,7 @@ const defaultMethods = { if (typeof data === 'number') return -data if (typeof data === 'boolean') return -data if (typeof data === 'object' && !Array.isArray(data)) return Number.NaN + if (data[0] && typeof data[0] === 'object') return Number.NaN if (data.length === 1) return -data[0] let res = data[0] for (let i = 1; i < data.length; i++) { diff --git a/suites/divide.json b/suites/divide.json index 6dcaa60..32740dd 100644 --- a/suites/divide.json +++ b/suites/divide.json @@ -164,5 +164,11 @@ "rule": { "/": [1, "a"] }, "result": { "error": "NaN" }, "data": null + }, + { + "description": "Divide with Array produces NaN", + "rule": { "/": [1, [1]] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/errors.json b/suites/errors.json index 9aca656..600afbc 100644 --- a/suites/errors.json +++ b/suites/errors.json @@ -23,5 +23,11 @@ "rule": { "??": [{ "+": [{ "val": "hello" }]}, 1] }, "result": 1, "data": { "hello": "world" } + }, + { + "description": "Errors are just data", + "rule": { "??": [{ "val": "x" }, 1]}, + "data": { "x": { "error": "Some error" }}, + "result": 1 } ] \ No newline at end of file diff --git a/suites/minus.json b/suites/minus.json index ae4b404..3c0cdd7 100644 --- a/suites/minus.json +++ b/suites/minus.json @@ -126,5 +126,11 @@ "rule": { "-": ["Hey", 1] }, "result": { "error": "NaN" }, "data": null + }, + { + "description": "Subtraction with Array produces NaN", + "rule": { "-": [[1], 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/modulo.json b/suites/modulo.json index 261c34d..4b82954 100644 --- a/suites/modulo.json +++ b/suites/modulo.json @@ -169,5 +169,11 @@ "rule": { "%": ["Hey", 1] }, "result": { "error": "NaN" }, "data": null + }, + { + "description": "Modulo with array produces NaN", + "rule": { "%": [[1], 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/multiply.json b/suites/multiply.json index 4f354df..b43ca5d 100644 --- a/suites/multiply.json +++ b/suites/multiply.json @@ -156,5 +156,11 @@ "rule": { "*": ["Hey", 1] }, "result": { "error": "NaN" }, "data": null + }, + { + "description": "Multiply with Array produces NaN", + "rule": { "*": [[1], 1] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file diff --git a/suites/plus.json b/suites/plus.json index 0350fd1..d019d39 100644 --- a/suites/plus.json +++ b/suites/plus.json @@ -170,5 +170,17 @@ "rule": { "+": [{ "val": "x" }, 1] }, "result": { "error": "NaN" }, "data": { "x": {} } + }, + { + "description": "Plus Operator with Single Operand, Invalid String Produces NaN", + "rule": { "+": "Hello" }, + "result": { "error": "NaN" }, + "data": null + }, + { + "description": "Plus Operator with Single Operand, Array Input Produces NaN", + "rule": { "+": [[1]] }, + "result": { "error": "NaN" }, + "data": null } ] \ No newline at end of file From 5618bf9c42bd1a8eb2e19d26dc89bf7b718423de Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 12:39:36 -0600 Subject: [PATCH 08/15] Switch from throws to panic --- defaultMethods.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index f252fcb..9f51b4e 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -111,7 +111,7 @@ const defaultMethods = { if (type === 'NaN') return Number.NaN return { error: type } }, - throws: (item) => { + panic: (item) => { if (Array.isArray(item)) item = item[0] if (Number.isNaN(item)) throw new Error('NaN was returned from expression') if (item && item.error) throw item.error @@ -967,7 +967,7 @@ defaultMethods['!!'].compile = function (data, buildState) { defaultMethods.none.deterministic = defaultMethods.some.deterministic // @ts-ignore Allowing a optimizeUnary attribute that can be used for performance optimizations -defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = defaultMethods.throws.optimizeUnary = true +defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = defaultMethods.panic.optimizeUnary = true export default { ...defaultMethods, From 1c77b906eece52ef03a92b4ac6089d6b60820433 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 14:32:36 -0600 Subject: [PATCH 09/15] Switch from `??` to `try` --- defaultMethods.js | 99 ++++++++++++++++++++++++++-------------------- suites/errors.json | 6 +-- 2 files changed, 59 insertions(+), 46 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index 9f51b4e..7ef9fd3 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -247,49 +247,6 @@ const defaultMethods = { xor: ([a, b]) => a ^ b, // Why "executeInLoop"? Because if it needs to execute to get an array, I do not want to execute the arguments, // Both for performance and safety reasons. - '??': { - [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), - method: (arr, _1, _2, engine) => { - // See "executeInLoop" above - const executeInLoop = Array.isArray(arr) - if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 }) - - let item - for (let i = 0; i < arr.length; i++) { - item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] - if (downgrade(item) !== null && item !== undefined) return item - } - - if (item === undefined) return null - return item - }, - asyncMethod: async (arr, _1, _2, engine) => { - // See "executeInLoop" above - const executeInLoop = Array.isArray(arr) - if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 }) - - let item - for (let i = 0; i < arr.length; i++) { - item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] - if (downgrade(item) !== null && item !== undefined) return item - } - - if (item === undefined) return null - return item - }, - deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), - compile: (data, buildState) => { - if (!chainingSupported) return false - if (Array.isArray(data) && data.length) { - return `(${data.map((i, x) => { - if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return buildString(i, buildState) - return 'downgrade(' + buildString(i, buildState) + ')' - }).join(' ?? ')})` - } - return `(${buildString(data, buildState)}).reduce((a,b) => downgrade(a) ?? b, null)` - }, - traverse: false - }, or: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { @@ -326,6 +283,8 @@ const defaultMethods = { }, traverse: false }, + '??': defineCoalesce(), + try: defineCoalesce(downgrade), and: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { @@ -719,6 +678,60 @@ const defaultMethods = { } } +/** + * Defines separate coalesce methods + */ +function defineCoalesce (func) { + let downgrade + if (func) downgrade = func + else downgrade = (a) => a + + return { + [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), + method: (arr, _1, _2, engine) => { + // See "executeInLoop" above + const executeInLoop = Array.isArray(arr) + if (!executeInLoop) arr = engine.run(arr, _1, { above: _2 }) + + let item + for (let i = 0; i < arr.length; i++) { + item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] + if (downgrade(item) !== null && item !== undefined) return item + } + + if (item === undefined) return null + return item + }, + asyncMethod: async (arr, _1, _2, engine) => { + // See "executeInLoop" above + const executeInLoop = Array.isArray(arr) + if (!executeInLoop) arr = await engine.run(arr, _1, { above: _2 }) + + let item + for (let i = 0; i < arr.length; i++) { + item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] + if (downgrade(item) !== null && item !== undefined) return item + } + + if (item === undefined) return null + return item + }, + deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), + compile: (data, buildState) => { + if (!chainingSupported) return false + const funcCall = func ? 'downgrade' : '' + if (Array.isArray(data) && data.length) { + return `(${data.map((i, x) => { + if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return buildString(i, buildState) + return `${funcCall}(` + buildString(i, buildState) + ')' + }).join(' ?? ')})` + } + return `(${buildString(data, buildState)}).reduce((a,b) => ${funcCall}(a) ?? b, null)` + }, + traverse: false + } +} + function createArrayIterativeMethod (name, useTruthy = false) { return { deterministic: (data, buildState) => { diff --git a/suites/errors.json b/suites/errors.json index 600afbc..dd4ecac 100644 --- a/suites/errors.json +++ b/suites/errors.json @@ -14,19 +14,19 @@ }, { "description": "Coalesce an error", - "rule": { "??": [{ "error": "Some error" }, 1] }, + "rule": { "try": [{ "error": "Some error" }, 1] }, "result": 1, "data": null }, { "description": "Coalesce an emitted error", - "rule": { "??": [{ "+": [{ "val": "hello" }]}, 1] }, + "rule": { "try": [{ "+": [{ "val": "hello" }]}, 1] }, "result": 1, "data": { "hello": "world" } }, { "description": "Errors are just data", - "rule": { "??": [{ "val": "x" }, 1]}, + "rule": { "try": [{ "val": "x" }, 1]}, "data": { "x": { "error": "Some error" }}, "result": 1 } From f6ec3d6615e988261f050486c3e82712c299d3f8 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 15:42:58 -0600 Subject: [PATCH 10/15] Add optimization to and/or that was previously making me hesitant to switch to compatible: true --- defaultMethods.js | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index 7ef9fd3..045d9b1 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -277,7 +277,15 @@ const defaultMethods = { }, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { - if (!buildState.engine.truthy[OriginalImpl]) return false + if (!buildState.engine.truthy[OriginalImpl]) { + let res = buildState.compile`` + if (Array.isArray(data) && data.length) { + for (let i = 0; i < data.length; i++) res = buildState.compile`${res} engine.truthy(prev = ${data[i]}) ? prev : ` + res = buildState.compile`${res} prev` + return res + } + return false + } if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' || ')})` return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)` }, @@ -314,7 +322,15 @@ const defaultMethods = { traverse: false, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { - if (!buildState.engine.truthy[OriginalImpl]) return false + if (!buildState.engine.truthy[OriginalImpl]) { + let res = buildState.compile`` + if (Array.isArray(data) && data.length) { + for (let i = 0; i < data.length; i++) res = buildState.compile`${res} !engine.truthy(prev = ${data[i]}) ? prev : ` + res = buildState.compile`${res} prev` + return res + } + return false + } if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' && ')})` return `(${buildString(data, buildState)}).reduce((a,b) => a&&b, true)` } From 2a0815319a8f66f8c964e04b4084cc8a76d0f49e Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 16:20:08 -0600 Subject: [PATCH 11/15] Use prev to optimize functions a bit further --- defaultMethods.js | 18 ++++++------------ legacy.js | 6 ++---- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index 045d9b1..b7c8470 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -931,13 +931,13 @@ function numberCoercion (i, buildState) { defaultMethods['+'].compile = function (data, buildState) { if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' + ')})` if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `(+${buildString(data, buildState)})` - return `([].concat(${buildString(data, buildState)})).reduce((a,b) => (+a)+(+precoerceNumber(b)), 0)` + return buildState.compile`(Array.isArray(prev = ${data}) ? prev.reduce((a,b) => (+a)+(+precoerceNumber(b)), 0) : +precoerceNumber(prev))` } // @ts-ignore Allow custom attribute defaultMethods['%'].compile = function (data, buildState) { if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' % ')})` - return `(${buildString(data, buildState)}).reduce((a,b) => (+a)%(+precoerceNumber(b)))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))%(+precoerceNumber(b)))` } // @ts-ignore Allow custom attribute @@ -949,24 +949,18 @@ defaultMethods.in.compile = function (data, buildState) { // @ts-ignore Allow custom attribute defaultMethods['-'].compile = function (data, buildState) { if (Array.isArray(data)) return `${data.length === 1 ? '-' : ''}(${data.map(i => numberCoercion(i, buildState)).join(' - ')})` - if (typeof data === 'string' || typeof data === 'number') { - return `(-${buildString(data, buildState)})` - } else { - return `((a=>(a.length===1?a[0]=-a[0]:a)&0||a)([].concat(${buildString( - data, - buildState - )}))).reduce((a,b) => (+a)-(+b))` - } + if (typeof data === 'string' || typeof data === 'number') return `(-${buildString(data, buildState)})` + return buildState.compile`(Array.isArray(prev = ${data}) ? prev.length === 1 ? -precoerceNumber(prev[0]) : prev.reduce((a,b) => (+precoerceNumber(a))-(+precoerceNumber(b))) : -precoerceNumber(prev))` } // @ts-ignore Allow custom attribute defaultMethods['/'].compile = function (data, buildState) { if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' / ')})` - return `(${buildString(data, buildState)}).reduce((a,b) => (+a)/(+b))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b)))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) { if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' * ')})` - return `(${buildString(data, buildState)}).reduce((a,b) => (+a)*(+b))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))*(+precoerceNumber(b)))` } // @ts-ignore Allow custom attribute defaultMethods.cat.compile = function (data, buildState) { diff --git a/legacy.js b/legacy.js index 98ea589..e42099a 100644 --- a/legacy.js +++ b/legacy.js @@ -4,8 +4,7 @@ import { splitPathMemoized } from './utilities/splitPath.js' import chainingSupported from './utilities/chainingSupported.js' import { Sync, OriginalImpl } from './constants.js' - -/** @type {Record<'get' | 'missing' | 'missing_some' | 'var', { method: (...args) => any }>} **/ +/** @type {Record<'get' | 'missing' | 'missing_some' | 'var', { method: (...args) => any }>} **/ const legacyMethods = { get: { [Sync]: true, @@ -159,5 +158,4 @@ const legacyMethods = { } } - -export default {...legacyMethods} +export default { ...legacyMethods } From 0b20892be9c71831eedddf570daa09dc7a22c732 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 16:22:53 -0600 Subject: [PATCH 12/15] Cat was doubly defined --- defaultMethods.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index b7c8470..e8892ec 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -962,14 +962,6 @@ defaultMethods['*'].compile = function (data, buildState) { if (Array.isArray(data)) return `(${data.map(i => numberCoercion(i, buildState)).join(' * ')})` return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))*(+precoerceNumber(b)))` } -// @ts-ignore Allow custom attribute -defaultMethods.cat.compile = function (data, buildState) { - if (typeof data === 'string') return JSON.stringify(data) - if (!Array.isArray(data)) return false - let res = buildState.compile`''` - for (let i = 0; i < data.length; i++) res = buildState.compile`${res} + ${data[i]}` - return buildState.compile`(${res})` -} // @ts-ignore Allow custom attribute defaultMethods['!'].compile = function ( From a9641d5d4bb928ee8d76807ae8e4989ee197d39e Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 16:41:09 -0600 Subject: [PATCH 13/15] Use prev to eliminate `methods.preventFunctions` --- compiler.js | 2 +- defaultMethods.js | 10 +++++----- legacy.js | 13 +++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/compiler.js b/compiler.js index 31b4280..51e83d1 100644 --- a/compiler.js +++ b/compiler.js @@ -308,7 +308,7 @@ function processBuiltString (method, str, buildState) { str = str.replace(`__%%%${x}%%%__`, item) }) - const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { let prev; const result = ${str}; return result }` + const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber) => ${buildState.asyncDetected ? 'async' : ''} (context ${buildState.extraArguments ? ',' + buildState.extraArguments : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }` // console.log(str) // console.log(final) // eslint-disable-next-line no-eval diff --git a/defaultMethods.js b/defaultMethods.js index e8892ec..d68d49e 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -404,13 +404,13 @@ const defaultMethods = { }, compile: (data, buildState) => { function wrapNull (data) { - if (!chainingSupported) return buildState.compile`(methods.preventFunctions(((a) => a === null || a === undefined ? null : a)(${data})))` - return buildState.compile`(methods.preventFunctions(${data} ?? null))` + let res + if (!chainingSupported) res = buildState.compile`(((a) => a === null || a === undefined ? null : a)(${data}))` + else res = buildState.compile`(${data} ?? null)` + if (!buildState.engine.allowFunctions) res = buildState.compile`(typeof (prev = ${res}) === 'function' ? null : prev)` + return res } - if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a - else buildState.methods.preventFunctions = a => a - if (typeof data === 'object' && !Array.isArray(data)) { // If the input for this function can be inlined, we will do so right here. if (isSyncDeep(data, buildState.engine, buildState) && isDeterministic(data, buildState.engine, buildState) && !buildState.engine.disableInline) data = (buildState.engine.fallback || buildState.engine).run(data, buildState.context, { above: buildState.above }) diff --git a/legacy.js b/legacy.js index e42099a..85b3cd3 100644 --- a/legacy.js +++ b/legacy.js @@ -116,19 +116,20 @@ const legacyMethods = { const pieces = splitPathMemoized(key) - if (!buildState.engine.allowFunctions) buildState.methods.preventFunctions = a => typeof a === 'function' ? null : a - else buildState.methods.preventFunctions = a => a - // support older versions of node if (!chainingSupported) { - return `(methods.preventFunctions(((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( + const res = `((((a,b) => (typeof a === 'undefined' || a === null) ? b : a)(${pieces.reduce( (text, i) => `(${text}||0)[${JSON.stringify(i)}]`, '(context||0)' )}, ${buildString(defaultValue, buildState)})))` + if (buildState.engine.allowFunctions) return res + return `(typeof (prev = ${res}) === 'function' ? null : prev)` } - return `(methods.preventFunctions(context${pieces + const res = `(context${pieces .map((i) => `?.[${JSON.stringify(i)}]`) - .join('')} ?? ${buildString(defaultValue, buildState)}))` + .join('')} ?? ${buildString(defaultValue, buildState)})` + if (buildState.engine.allowFunctions) return res + return `(typeof (prev = ${res}) === 'function' ? null : prev)` } return false } From 17c2dc75e786d34ad08376709f00dc861cb6edae Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 18:56:50 -0600 Subject: [PATCH 14/15] Code cleanup and optimizations --- asyncLogic.js | 13 ++++++------- async_optimizer.js | 2 +- compatibility.js | 2 +- compiler.js | 8 ++++---- defaultMethods.js | 27 +++++++++++++-------------- logic.js | 22 +++++++++++++--------- optimizer.js | 4 ++-- 7 files changed, 40 insertions(+), 38 deletions(-) diff --git a/asyncLogic.js b/asyncLogic.js index fb1d95e..7e6d353 100644 --- a/asyncLogic.js +++ b/asyncLogic.js @@ -89,9 +89,8 @@ class AsyncLogicEngine { } if (typeof this.methods[func] === 'object') { - const { asyncMethod, method, traverse } = this.methods[func] - const shouldTraverse = typeof traverse === 'undefined' ? true : traverse - const parsedData = shouldTraverse ? ((!data || typeof data !== 'object') ? [data] : coerceArray(await this.run(data, context, { above }))) : data + const { asyncMethod, method, lazy } = this.methods[func] + const parsedData = !lazy ? ((!data || typeof data !== 'object') ? [data] : coerceArray(await this.run(data, context, { above }))) : data const result = await (asyncMethod || method)(parsedData, context, above, this) return Array.isArray(result) ? Promise.all(result) : result } @@ -102,7 +101,7 @@ class AsyncLogicEngine { /** * * @param {String} name The name of the method being added. - * @param {((args: any, context: any, above: any[], engine: AsyncLogicEngine) => any) | { traverse?: Boolean, method?: (args: any, context: any, above: any[], engine: AsyncLogicEngine) => any, asyncMethod?: (args: any, context: any, above: any[], engine: AsyncLogicEngine) => Promise, deterministic?: Function | Boolean }} method + * @param {((args: any, context: any, above: any[], engine: AsyncLogicEngine) => any) | { lazy?: Boolean, traverse?: Boolean, method?: (args: any, context: any, above: any[], engine: AsyncLogicEngine) => any, asyncMethod?: (args: any, context: any, above: any[], engine: AsyncLogicEngine) => Promise, deterministic?: Function | Boolean }} method * @param {{ deterministic?: Boolean, async?: Boolean, sync?: Boolean, optimizeUnary?: boolean }} annotations This is used by the compiler to help determine if it can optimize the function being generated. */ addMethod ( @@ -115,9 +114,9 @@ class AsyncLogicEngine { if (typeof async !== 'undefined') sync = !async if (typeof method === 'function') { - if (async) method = { asyncMethod: method, traverse: true } - else method = { method, traverse: true } - } else method = { ...method } + if (async) method = { asyncMethod: method, lazy: false } + else method = { method, lazy: false } + } else method = { ...method, lazy: typeof method.traverse !== 'undefined' ? !method.traverse : method.lazy } Object.assign(method, omitUndefined({ deterministic, optimizeUnary })) // @ts-ignore diff --git a/async_optimizer.js b/async_optimizer.js index 7c27584..0d2d61b 100644 --- a/async_optimizer.js +++ b/async_optimizer.js @@ -17,7 +17,7 @@ function getMethod (logic, engine, methodName, above) { const method = engine.methods[methodName] const called = method.asyncMethod ? method.asyncMethod : method.method ? method.method : method - if (method.traverse === false) { + if (method.lazy) { if (typeof method[Sync] === 'function' && method[Sync](logic, { engine })) { const called = method.method ? method.method : method return declareSync((data, abv) => called(logic[methodName], data, abv || above, engine.fallback), true) diff --git a/compatibility.js b/compatibility.js index acacec0..e57ea2e 100644 --- a/compatibility.js +++ b/compatibility.js @@ -19,7 +19,7 @@ const all = { return oldAll.asyncMethod(args, context, above, engine) }, deterministic: oldAll.deterministic, - traverse: oldAll.traverse + lazy: oldAll.lazy } function truthy (value) { diff --git a/compiler.js b/compiler.js index 51e83d1..e72eade 100644 --- a/compiler.js +++ b/compiler.js @@ -88,7 +88,7 @@ export function isDeterministic (method, engine, buildState) { if (lower === undefined) return true if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) - if (engine.methods[func].traverse === false) { + if (engine.methods[func].lazy) { return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic @@ -119,7 +119,7 @@ function isDeepSync (method, engine) { const lower = method[func] if (!isSync(engine.methods[func])) return false - if (engine.methods[func].traverse === false) { + if (engine.methods[func].lazy) { if (typeof engine.methods[func][Sync] === 'function' && engine.methods[func][Sync](method, { engine })) return true return false } @@ -194,7 +194,7 @@ function buildString (method, buildState = {}) { } let lower = method[func] - if ((!lower || typeof lower !== 'object') && (typeof engine.methods[func].traverse === 'undefined' || engine.methods[func].traverse)) lower = [lower] + if ((!lower || typeof lower !== 'object') && (!engine.methods[func].lazy)) lower = [lower] if (engine.methods[func] && engine.methods[func].compile) { let str = engine.methods[func].compile(lower, buildState) @@ -220,7 +220,7 @@ function buildString (method, buildState = {}) { const argCount = countArguments(asyncDetected ? engine.methods[func].asyncMethod : engine.methods[func].method) const argumentsNeeded = argumentsDict[argCount - 1] || argumentsDict[2] - if (engine.methods[func] && (typeof engine.methods[func].traverse === 'undefined' ? true : engine.methods[func].traverse)) { + if (engine.methods[func] && !engine.methods[func].lazy) { return makeAsync(`engine.methods["${func}"]${asyncDetected ? '.asyncMethod' : '.method'}(${coerce}(` + buildString(lower, buildState) + ')' + argumentsNeeded + ')') } else { notTraversed.push(lower) diff --git a/defaultMethods.js b/defaultMethods.js index d68d49e..dbfcd9c 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -21,11 +21,12 @@ function isDeterministic (method, engine, buildState) { if (engine.isData(method, func)) return true if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) - if (engine.methods[func].traverse === false) { + if (engine.methods[func].lazy) { return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic } + return typeof engine.methods[func].deterministic === 'function' ? engine.methods[func].deterministic(lower, buildState) : engine.methods[func].deterministic && @@ -44,7 +45,7 @@ function isSyncDeep (method, engine, buildState) { const lower = method[func] if (engine.isData(method, func)) return true if (!engine.methods[func]) throw new Error(`Method '${func}' was not found in the Logic Engine.`) - if (engine.methods[func].traverse === false) return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] + if (engine.methods[func].lazy) return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] return typeof engine.methods[func][Sync] === 'function' ? engine.methods[func][Sync](lower, buildState) : engine.methods[func][Sync] && isSyncDeep(lower, engine, buildState) } @@ -121,7 +122,7 @@ const defaultMethods = { min: (data) => Math.min(...data), in: ([item, array]) => (array || []).includes(item), preserve: { - traverse: false, + lazy: true, method: declareSync((i) => i, true), [Sync]: () => true }, @@ -182,7 +183,7 @@ const defaultMethods = { return engine.run(onFalse, context, { above }) }, - traverse: false + lazy: true }, '<': (args) => { if (args.length === 2) return args[0] < args[1] @@ -289,7 +290,7 @@ const defaultMethods = { if (Array.isArray(data) && data.length) return `(${data.map((i) => buildString(i, buildState)).join(' || ')})` return `(${buildString(data, buildState)}).reduce((a,b) => a||b, false)` }, - traverse: false + lazy: true }, '??': defineCoalesce(), try: defineCoalesce(downgrade), @@ -319,7 +320,7 @@ const defaultMethods = { } return item }, - traverse: false, + lazy: true, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), compile: (data, buildState) => { if (!buildState.engine.truthy[OriginalImpl]) { @@ -352,7 +353,6 @@ const defaultMethods = { const result = defaultMethods.val.method(key, context, above, engine, Unfound) return result !== Unfound }, - traverse: true, deterministic: false }, val: { @@ -444,7 +444,7 @@ const defaultMethods = { all: createArrayIterativeMethod('every', true), none: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), - traverse: false, + lazy: true, // todo: add async build & build method: (val, context, above, engine) => { return !defaultMethods.some.method(val, context, above, engine) @@ -580,7 +580,7 @@ const defaultMethods = { defaultValue ) }, - traverse: false + lazy: true }, '!': (value, _1, _2, engine) => Array.isArray(value) ? !engine.truthy(value[0]) : !engine.truthy(value), '!!': (value, _1, _2, engine) => Boolean(Array.isArray(value) ? engine.truthy(value[0]) : engine.truthy(value)), @@ -598,7 +598,6 @@ const defaultMethods = { return res }, deterministic: true, - traverse: true, optimizeUnary: true, compile: (data, buildState) => { if (typeof data === 'string') return JSON.stringify(data) @@ -611,7 +610,7 @@ const defaultMethods = { }, keys: ([obj]) => typeof obj === 'object' ? Object.keys(obj) : [], pipe: { - traverse: false, + lazy: true, [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (args, context, above, engine) => { if (!Array.isArray(args)) throw new Error('Data for pipe must be an array') @@ -638,7 +637,7 @@ const defaultMethods = { } }, eachKey: { - traverse: false, + lazy: true, [Sync]: (data, buildState) => isSyncDeep(Object.values(data[Object.keys(data)[0]]), buildState.engine, buildState), method: (object, context, above, engine) => { const result = Object.keys(object).reduce((accumulator, key) => { @@ -744,7 +743,7 @@ function defineCoalesce (func) { } return `(${buildString(data, buildState)}).reduce((a,b) => ${funcCall}(a) ?? b, null)` }, - traverse: false + lazy: true } } @@ -814,7 +813,7 @@ function createArrayIterativeMethod (name, useTruthy = false) { return buildState.compile`(${selector} || [])[${name}]((i, x, z) => ${useTruthyMethod}(${method}(i, x, ${aboveArray})))` }, - traverse: false + lazy: true } } defaultMethods['?:'] = defaultMethods.if diff --git a/logic.js b/logic.js index 3348d22..f861528 100644 --- a/logic.js +++ b/logic.js @@ -64,8 +64,7 @@ class LogicEngine { * @param {*} above The context above (can be used for handlebars-style data traversal.) * @returns {{ result: *, func: string }} */ - _parse (logic, context, above) { - const [func] = Object.keys(logic) + _parse (logic, context, above, func) { const data = logic[func] if (this.isData(logic, func)) return logic @@ -85,9 +84,8 @@ class LogicEngine { } if (typeof this.methods[func] === 'object') { - const { method, traverse } = this.methods[func] - const shouldTraverse = typeof traverse === 'undefined' ? true : traverse - const parsedData = shouldTraverse ? ((!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above }))) : data + const { method, lazy } = this.methods[func] + const parsedData = !lazy ? ((!data || typeof data !== 'object') ? [data] : coerceArray(this.run(data, context, { above }))) : data return method(parsedData, context, above, this) } @@ -97,12 +95,12 @@ class LogicEngine { /** * * @param {String} name The name of the method being added. - * @param {((args: any, context: any, above: any[], engine: LogicEngine) => any) |{ traverse?: Boolean, method: (args: any, context: any, above: any[], engine: LogicEngine) => any, deterministic?: Function | Boolean }} method + * @param {((args: any, context: any, above: any[], engine: LogicEngine) => any) |{ lazy?: Boolean, traverse?: Boolean, method: (args: any, context: any, above: any[], engine: LogicEngine) => any, deterministic?: Function | Boolean }} method * @param {{ deterministic?: Boolean, optimizeUnary?: Boolean }} annotations This is used by the compiler to help determine if it can optimize the function being generated. */ addMethod (name, method, { deterministic, optimizeUnary } = {}) { - if (typeof method === 'function') method = { method, traverse: true } - else method = { ...method } + if (typeof method === 'function') method = { method, lazy: false } + else method = { ...method, lazy: typeof method.traverse !== 'undefined' ? !method.traverse : method.lazy } Object.assign(method, omitUndefined({ deterministic, optimizeUnary })) this.methods[name] = declareSync(method) } @@ -161,7 +159,13 @@ class LogicEngine { return res } - if (logic && typeof logic === 'object' && Object.keys(logic).length > 0) return this._parse(logic, data, above) + if (logic && typeof logic === 'object') { + const keys = Object.keys(logic) + if (keys.length > 0) { + const func = keys[0] + return this._parse(logic, data, above, func) + } + } return logic } diff --git a/optimizer.js b/optimizer.js index b9dc5cf..8e2f826 100644 --- a/optimizer.js +++ b/optimizer.js @@ -14,7 +14,7 @@ function getMethod (logic, engine, methodName, above) { const method = engine.methods[methodName] const called = method.method ? method.method : method - if (method.traverse === false) { + if (method.lazy) { const args = logic[methodName] return (data, abv) => called(args, data, abv || above, engine) } @@ -30,7 +30,7 @@ function getMethod (logic, engine, methodName, above) { return called(evaluatedArgs, data, abv || above, engine) } } else { - let optimizedArgs = optimize(args, engine, above) + const optimizedArgs = optimize(args, engine, above) if (method.optimizeUnary) { if (typeof optimizedArgs === 'function') return (data, abv) => called(optimizedArgs(data, abv), data, abv || above, engine) return (data, abv) => called(optimizedArgs, data, abv || above, engine) From 1287baf3188a90570fbc4a756ebe4504e04d2852 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Sat, 18 Jan 2025 19:25:49 -0600 Subject: [PATCH 15/15] Use new Array micro optimization --- asyncLogic.js | 4 ++-- logic.js | 4 ++-- utilities/coerceArray.js | 3 +-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/asyncLogic.js b/asyncLogic.js index 7e6d353..5f90f56 100644 --- a/asyncLogic.js +++ b/asyncLogic.js @@ -173,10 +173,10 @@ class AsyncLogicEngine { // END OPTIMIZER BLOCK // if (Array.isArray(logic)) { - const res = [] + const res = new Array(logic.length) // Note: In the past, it used .map and Promise.all; this can be changed in the future // if we want it to run concurrently. - for (let i = 0; i < logic.length; i++) res.push(await this.run(logic[i], data, { above })) + for (let i = 0; i < logic.length; i++) res[i] = await this.run(logic[i], data, { above }) return res } diff --git a/logic.js b/logic.js index f861528..44d8379 100644 --- a/logic.js +++ b/logic.js @@ -154,8 +154,8 @@ class LogicEngine { // END OPTIMIZER BLOCK // if (Array.isArray(logic)) { - const res = [] - for (let i = 0; i < logic.length; i++) res.push(this.run(logic[i], data, { above })) + const res = new Array(logic.length) + for (let i = 0; i < logic.length; i++) res[i] = this.run(logic[i], data, { above }) return res } diff --git a/utilities/coerceArray.js b/utilities/coerceArray.js index 0c80880..1325ffe 100644 --- a/utilities/coerceArray.js +++ b/utilities/coerceArray.js @@ -3,7 +3,6 @@ * Coerces a value into an array. * This is used for unary value operations. */ -export function coerceArray (value, skip = false) { - if (skip) return value +export function coerceArray (value) { return Array.isArray(value) ? value : [value] }