From 342409450aa6fbae4d59237fd0cab64760f9ecde Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 12:27:16 -0600 Subject: [PATCH 1/8] Create a branch that uses hard exceptions instead --- defaultMethods.js | 83 ++++++++++++++++++++++++++++++------------ general.test.js | 4 +- suites/divide.json | 12 +++--- suites/error.json | 14 +++++++ suites/minus.json | 6 +-- suites/modulo.json | 6 +-- suites/multiply.json | 6 +-- suites/panic.json | 32 ---------------- suites/plus.json | 18 ++++----- suites/truthiness.json | 13 ------- suites/try.json | 30 ++++++++++++--- utilities/downgrade.js | 2 +- 12 files changed, 125 insertions(+), 101 deletions(-) create mode 100644 suites/error.json delete mode 100644 suites/panic.json diff --git a/defaultMethods.js b/defaultMethods.js index 2cf48b0..a26e110 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -59,10 +59,10 @@ 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 (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') let res = 0 for (let i = 0; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') return Number.NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res += +data[i] } return res @@ -70,16 +70,16 @@ const defaultMethods = { '*': (data) => { let res = 1 for (let i = 0; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') return Number.NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res *= +data[i] } return res }, '/': (data) => { - if (data[0] && typeof data[0] === 'object') return Number.NaN + if (data[0] && typeof data[0] === 'object') throw new Error('NaN') let res = +data[0] for (let i = 1; i < data.length; i++) { - if ((data[i] && typeof data[i] === 'object') || !data[i]) return Number.NaN + if ((data[i] && typeof data[i] === 'object') || !data[i]) throw new Error('NaN') res /= +data[i] } return res @@ -89,35 +89,27 @@ 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[0] && typeof data[0] === 'object') return Number.NaN + if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') + if (data[0] && typeof data[0] === 'object') throw new Error('NaN') if (data.length === 1) return -data[0] let res = data[0] for (let i = 1; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') return Number.NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res -= +data[i] } return res }, '%': (data) => { - if (data[0] && typeof data[0] === 'object') return Number.NaN + if (data[0] && typeof data[0] === 'object') throw new Error('NaN') let res = +data[0] for (let i = 1; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') return Number.NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res %= +data[i] } return res }, error: (type) => { - if (Array.isArray(type)) type = type[0] - if (type === 'NaN') return Number.NaN - return { error: type } - }, - 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 - return item + throw new Error(type) }, max: (data) => Math.max(...data), min: (data) => Math.min(...data), @@ -290,7 +282,52 @@ const defaultMethods = { lazy: true }, '??': defineCoalesce(), - try: defineCoalesce(downgrade, true), + try: { + [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 + let lastError + for (let i = 0; i < arr.length; i++) { + try { + item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] + return item + } catch (e) { + // Do nothing + lastError = e + } + } + + throw lastError + }, + 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 + let lastError + for (let i = 0; i < arr.length; i++) { + try { + item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] + return item + } catch (e) { + // Do nothing + lastError = e + } + } + + throw lastError + }, + deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), + compile: (data, buildState) => { + return false + }, + lazy: true + }, and: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { @@ -933,11 +970,11 @@ defaultMethods['/'].compile = function (data, buildState) { if (Array.isArray(data)) { return `(${data.map((i, x) => { let res = numberCoercion(i, buildState) - if (x) res = `(${res}||NaN)` + if (x) res = `(${res}|| (() => { throw new Error() })() )` return res }).join(' / ')})` } - return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || NaN))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw new Error() })() ))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) { @@ -964,7 +1001,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.panic.optimizeUnary = true +defaultMethods['+'].optimizeUnary = defaultMethods['-'].optimizeUnary = defaultMethods['!'].optimizeUnary = defaultMethods['!!'].optimizeUnary = defaultMethods.cat.optimizeUnary = defaultMethods.error.optimizeUnary = true export default { ...defaultMethods, diff --git a/general.test.js b/general.test.js index cd48987..e038c1c 100644 --- a/general.test.js +++ b/general.test.js @@ -279,14 +279,14 @@ describe('Various Test Cases', () => { it('should throw on a soft error when panic is used', async () => { const rule = { - panic: { '+': 'hi' } + try: { '+': 'hi' } } for (const engine of normalEngines) await testEngine(engine, rule, {}, Error) for (const engine of permissiveEngines) await testEngine(engine, rule, {}, Error) const rule2 = { - panic: { error: 'Yeet' } + try: { error: 'Yeet' } } for (const engine of normalEngines) await testEngine(engine, rule2, {}, Error) diff --git a/suites/divide.json b/suites/divide.json index 2778d36..61d95f5 100644 --- a/suites/divide.json +++ b/suites/divide.json @@ -150,37 +150,37 @@ { "description": "Divide by Zero", "rule": { "/": [0, 0] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Divide by NaN", "rule": { "/": [1, { "error": "NaN" }] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Divide with String produces NaN", "rule": { "/": [1, "a"] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Divide with Array produces NaN", "rule": { "/": [1, [1]] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Any division by zero should return NaN", "rule": { "/": [1, 0] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Any division by zero should return NaN (2)", "rule": { "/": [8, 2, 0] }, - "result": { "error": "NaN" }, + "error": true, "data": null } ] \ No newline at end of file diff --git a/suites/error.json b/suites/error.json new file mode 100644 index 0000000..ae10f6b --- /dev/null +++ b/suites/error.json @@ -0,0 +1,14 @@ +[ + { + "description": "Creates an error object", + "rule": { "error": "hello" }, + "data": null, + "error": true + }, + { + "description": "NaN creates an error object or measured equivalent (equivalence class test)", + "rule": { "error": "NaN" }, + "data": null, + "error": true + } +] \ No newline at end of file diff --git a/suites/minus.json b/suites/minus.json index 3c0cdd7..75ced97 100644 --- a/suites/minus.json +++ b/suites/minus.json @@ -118,19 +118,19 @@ { "description": "Subtraction with NaN", "rule": { "-": [{ "error": "NaN" }, 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Subtraction with string produces NaN", "rule": { "-": ["Hey", 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Subtraction with Array produces NaN", "rule": { "-": [[1], 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null } ] \ No newline at end of file diff --git a/suites/modulo.json b/suites/modulo.json index 4b82954..4eb30a6 100644 --- a/suites/modulo.json +++ b/suites/modulo.json @@ -161,19 +161,19 @@ { "description": "Modulo with NaN", "rule": { "%": [{ "error": "NaN" }, 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Modulo with string produces NaN", "rule": { "%": ["Hey", 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Modulo with array produces NaN", "rule": { "%": [[1], 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null } ] \ No newline at end of file diff --git a/suites/multiply.json b/suites/multiply.json index b43ca5d..2308624 100644 --- a/suites/multiply.json +++ b/suites/multiply.json @@ -148,19 +148,19 @@ { "description": "Multiply with NaN", "rule": { "*": [{ "error": "NaN" }, 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Multiply with string produces NaN", "rule": { "*": ["Hey", 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Multiply with Array produces NaN", "rule": { "*": [[1], 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null } ] \ No newline at end of file diff --git a/suites/panic.json b/suites/panic.json deleted file mode 100644 index 65d8184..0000000 --- a/suites/panic.json +++ /dev/null @@ -1,32 +0,0 @@ -[ - { - "description": "Can promote a soft error to a hard error with panic", - "rule": { "panic": { "error": "Some error" } }, - "error": true, - "data": null - }, - { - "description": "Panic with an error emitted from an operator", - "rule": { "panic": [{ "/": [1, 0] }] }, - "error": true, - "data": null - }, - { - "description": "Panic with an error pulled from context", - "rule": { "panic": [{ "val": "x" }] }, - "error": true, - "data": { "x": { "error": "Some error" } } - }, - { - "description": "Panic within an iterator", - "rule": { "map": [[1, 2, 3], { "panic": [{ "/": [0,0] }] }] }, - "error": true, - "data": null - }, - { - "description": "Panic if the user is not an admin", - "rule": { "panic": [{ "if": [{"val": ["user", "admin"]}, true, { "error": "Not an admin" }] }] }, - "data": { "user": { "admin": false } }, - "error": true - } -] \ No newline at end of file diff --git a/suites/plus.json b/suites/plus.json index 100bdf3..1f5a548 100644 --- a/suites/plus.json +++ b/suites/plus.json @@ -144,55 +144,55 @@ { "description": "Addition with NaN", "rule": { "+": [{ "error": "NaN" }, 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Addition with string produces NaN", "rule": { "+": ["Hey", 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Addition with Array produces NaN", "rule": { "+": [[1], 1] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Addition with Array from context produces NaN", "rule": { "+": [{ "val": "x" }, 1] }, - "result": { "error": "NaN" }, + "error": true, "data": { "x": [1] } }, { "description": "Addition with Object produces NaN", "rule": { "+": [{ "val": "x" }, 1] }, - "result": { "error": "NaN" }, + "error": true, "data": { "x": {} } }, { "description": "Plus Operator with Single Operand, Invalid String Produces NaN", "rule": { "+": "Hello" }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Plus Operator with Single Operand, Array Input Produces NaN", "rule": { "+": [[1]] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Plus Operator with Single Operand, Object Input Produces NaN", "rule": { "+": [{}] }, - "result": { "error": "NaN" }, + "error": true, "data": null }, { "description": "Plus Operator with Single Operand, Direct Object Input Produces NaN", "rule": { "+": {} }, - "result": { "error": "NaN" }, + "error": true, "data": null } ] \ No newline at end of file diff --git a/suites/truthiness.json b/suites/truthiness.json index 0e073e5..4026d1a 100644 --- a/suites/truthiness.json +++ b/suites/truthiness.json @@ -55,19 +55,6 @@ "result": false, "data": null }, - "# Some error tests", - { - "description": "Truthy: NaN", - "rule": { "!!": { "error": "NaN" } }, - "result": true, - "data": null - }, - { - "description": "Truthy: Arbitrary error", - "rule": { "!!": { "error": "Some error" } }, - "result": true, - "data": null - }, "# Context Object Tests", { "description": "Truthy: Zero Key Object", diff --git a/suites/try.json b/suites/try.json index a498c3e..c4185f5 100644 --- a/suites/try.json +++ b/suites/try.json @@ -12,12 +12,6 @@ "result": 1, "data": { "hello": "world" } }, - { - "description": "Coalesce an error pulled from context; errors are just data.", - "rule": { "try": [{ "val": "x" }, 1]}, - "data": { "x": { "error": "Some error" }}, - "result": 1 - }, { "description": "Panics if none of the values are valid", "rule": { "try": [{ "error": "Some error" }, { "error": "Some other error" }] }, @@ -29,5 +23,29 @@ "rule": { "try": [{ "error": "Some error" }, { "/": [0, 0] }] }, "error": true, "data": null + }, + { + "description": "Can promote a soft error to a hard error", + "rule": { "try": { "error": "Some error" } }, + "error": true, + "data": null + }, + { + "description": "Panic with an error emitted from an operator", + "rule": { "try": [{ "/": [1, 0] }] }, + "error": true, + "data": null + }, + { + "description": "Panic within an iterator", + "rule": { "map": [[1, 2, 3], { "try": [{ "/": [0,0] }] }] }, + "error": true, + "data": null + }, + { + "description": "Panic if the user is not an admin", + "rule": { "try": [{ "if": [{"val": ["user", "admin"]}, true, { "error": "Not an admin" }] }] }, + "data": { "user": { "admin": false } }, + "error": true } ] \ No newline at end of file diff --git a/utilities/downgrade.js b/utilities/downgrade.js index 9fb056e..53cc8e6 100644 --- a/utilities/downgrade.js +++ b/utilities/downgrade.js @@ -15,6 +15,6 @@ export function downgrade (item) { */ export function precoerceNumber (item) { if (!item) return item - if (typeof item === 'object') return Number.isNaN + if (typeof item === 'object') throw new Error('Cannot coerce object to number') return item } From 52e84e36c31963a8320178aaec72a319ee1d2cc3 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 12:32:23 -0600 Subject: [PATCH 2/8] Have a test that shows try working further up the AST --- suites/try.json | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/suites/try.json b/suites/try.json index c4185f5..637aeba 100644 --- a/suites/try.json +++ b/suites/try.json @@ -47,5 +47,19 @@ "rule": { "try": [{ "if": [{"val": ["user", "admin"]}, true, { "error": "Not an admin" }] }] }, "data": { "user": { "admin": false } }, "error": true + }, + { + "description": "Try can work further up the AST with Exceptions", + "rule": { + "try": [{ + "if": [ + true, + { "map": [[1,2,3], {"/": [0, 0] }]}, + null + ] + }, 10] + }, + "result": 10, + "data": null } ] \ No newline at end of file From 5fd338b133986e2b1cca5a588e40859db942eb08 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 13:29:49 -0600 Subject: [PATCH 3/8] Add context to try fallbacks --- defaultMethods.js | 12 ++++++++---- suites/try.json | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index a26e110..beee76f 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -293,7 +293,9 @@ const defaultMethods = { let lastError for (let i = 0; i < arr.length; i++) { try { - item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] + // Todo: make this message thing more robust. + if (lastError) item = engine.run(arr[i], { error: lastError.message || lastError.constructor.name }, { above: [null, _1, _2] }) + else item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] return item } catch (e) { // Do nothing @@ -312,7 +314,9 @@ const defaultMethods = { let lastError for (let i = 0; i < arr.length; i++) { try { - item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] + // Todo: make this message thing more robust. + if (lastError) item = await engine.run(arr[i], { error: lastError.message || lastError.constructor.name }, { above: [null, _1, _2] }) + else item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] return item } catch (e) { // Do nothing @@ -970,11 +974,11 @@ defaultMethods['/'].compile = function (data, buildState) { if (Array.isArray(data)) { return `(${data.map((i, x) => { let res = numberCoercion(i, buildState) - if (x) res = `(${res}|| (() => { throw new Error() })() )` + if (x) res = `(${res}|| (() => { throw new Error('NaN') })() )` return res }).join(' / ')})` } - return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw new Error() })() ))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw new Error('NaN') })() ))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) { diff --git a/suites/try.json b/suites/try.json index 637aeba..8450bc6 100644 --- a/suites/try.json +++ b/suites/try.json @@ -61,5 +61,45 @@ }, "result": 10, "data": null + }, + { + "description": "The context switches for the try coalescing to the previous error", + "rule": { + "try": [ + { "if": [true, { "error": "Some error" }, null] }, + { "val": [] } + ] + }, + "result": { "error": "Some error"}, + "data": null + }, + { + "description": "Try can work further up the AST with Exceptions, and return the error", + "rule": { + "try": [{ + "if": [ + true, + { "map": [[1,2,3], {"/": [0, 0] }]}, + null + ] + }, { "val": [] }] + }, + "result": { "error": "NaN" }, + "data": null + }, + "# Not Proposed", + { + "description": "Try can work further up the AST with Exceptions; Grabbing other Context value", + "rule": { + "try": [{ + "if": [ + true, + { "map": [[1,2,3], {"/": [0, 0] }]}, + null + ] + }, { "val": [[2], "fallback"] }] + }, + "result": "Hello", + "data": { "fallback": "Hello" } } ] \ No newline at end of file From 9d4f8bf8b3e94da1d47bf35862145484fd4b717a Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 13:32:36 -0600 Subject: [PATCH 4/8] Change error message --- utilities/downgrade.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/downgrade.js b/utilities/downgrade.js index 53cc8e6..bee1231 100644 --- a/utilities/downgrade.js +++ b/utilities/downgrade.js @@ -15,6 +15,6 @@ export function downgrade (item) { */ export function precoerceNumber (item) { if (!item) return item - if (typeof item === 'object') throw new Error('Cannot coerce object to number') + if (typeof item === 'object') throw new Error('NaN') return item } From bcae1299d609d5570c4a1eadacf42fa8e84862bd Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 15:23:43 -0600 Subject: [PATCH 5/8] Fix up the implementation --- compatible.test.js | 2 + compiler.js | 6 +- defaultMethods.js | 132 +++++++++++++++++++---------------------- suites/multiply.json | 6 ++ utilities/downgrade.js | 12 +--- 5 files changed, 72 insertions(+), 86 deletions(-) diff --git a/compatible.test.js b/compatible.test.js index df23adb..7e12f7b 100644 --- a/compatible.test.js +++ b/compatible.test.js @@ -56,6 +56,7 @@ describe('All of the compatible tests', () => { if (Array.isArray(result)) result = result.map(i => (i || 0).toNumber ? Number(i) : i) expect(correction(result)).toStrictEqual(testCase.result) } catch (err) { + if (err.message.includes('expect')) throw err expect(testCase.error).toStrictEqual(true) } }) @@ -70,6 +71,7 @@ describe('All of the compatible tests', () => { if (Array.isArray(result)) result = result.map(i => i.toNumber ? Number(i) : i) expect(correction(result)).toStrictEqual(testCase.result) } catch (err) { + if (err.message.includes('expect')) throw err expect(testCase.error).toStrictEqual(true) } }) diff --git a/compiler.js b/compiler.js index 94a41f2..b8bc485 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, precoerceNumber } from './utilities/downgrade.js' +import { precoerceNumber } from './utilities/downgrade.js' /** * Provides a simple way to compile logic into a function that can be run. @@ -310,12 +310,12 @@ 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 : ''}) => { ${str.includes('prev') ? 'let prev;' : ''} const result = ${str}; return result }` + const final = `(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, 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 return Object.assign( - (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, downgrade, precoerceNumber), { + (typeof globalThis !== 'undefined' ? globalThis : global).eval(final)(values, methods, notTraversed, asyncIterators, engine, above, coerceArray, precoerceNumber), { [Sync]: !buildState.asyncDetected, aboveDetected: typeof str === 'string' && str.includes(', above') }) diff --git a/defaultMethods.js b/defaultMethods.js index beee76f..23ecde0 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -8,7 +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' +import { precoerceNumber } from './utilities/downgrade.js' function isDeterministic (method, engine, buildState) { if (Array.isArray(method)) { @@ -56,15 +56,16 @@ const oldAll = createArrayIterativeMethod('every', true) const defaultMethods = { '+': (data) => { if (!data) return 0 - if (typeof data === 'string') return +data - if (typeof data === 'number') return +data - if (typeof data === 'boolean') return +data + if (typeof data === 'string') return precoerceNumber(+data) + if (typeof data === 'number') return precoerceNumber(+data) + if (typeof data === 'boolean') return precoerceNumber(+data) if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') let res = 0 for (let i = 0; i < data.length; i++) { if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res += +data[i] } + if (Number.isNaN(res)) throw new Error('NaN') return res }, '*': (data) => { @@ -73,6 +74,7 @@ const defaultMethods = { if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res *= +data[i] } + if (Number.isNaN(res)) throw new Error('NaN') return res }, '/': (data) => { @@ -82,13 +84,14 @@ const defaultMethods = { if ((data[i] && typeof data[i] === 'object') || !data[i]) throw new Error('NaN') res /= +data[i] } + if (Number.isNaN(res)) throw new Error('NaN') return res }, '-': (data) => { if (!data) return 0 - if (typeof data === 'string') return -data - if (typeof data === 'number') return -data - if (typeof data === 'boolean') return -data + if (typeof data === 'string') return precoerceNumber(-data) + if (typeof data === 'number') return precoerceNumber(-data) + if (typeof data === 'boolean') return precoerceNumber(-data) if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') if (data[0] && typeof data[0] === 'object') throw new Error('NaN') if (data.length === 1) return -data[0] @@ -97,6 +100,7 @@ const defaultMethods = { if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res -= +data[i] } + if (Number.isNaN(res)) throw new Error('NaN') return res }, '%': (data) => { @@ -106,6 +110,7 @@ const defaultMethods = { if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res %= +data[i] } + if (Number.isNaN(res)) throw new Error('NaN') return res }, error: (type) => { @@ -281,7 +286,51 @@ const defaultMethods = { }, lazy: true }, - '??': defineCoalesce(), + '??': { + [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 (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 (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) => { + const built = buildString(i, buildState) + if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built + return '(' + built + ')' + }).join(' ?? ')})` + } + return `(${buildString(data, buildState)}).reduce((a,b) => (a) ?? b, null)` + }, + lazy: true + }, try: { [Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState), method: (arr, _1, _2, engine) => { @@ -327,9 +376,6 @@ const defaultMethods = { throw lastError }, deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState), - compile: (data, buildState) => { - return false - }, lazy: true }, and: { @@ -753,64 +799,6 @@ const defaultMethods = { } } -/** - * Defines separate coalesce methods - */ -function defineCoalesce (func, panic) { - 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 - if (panic) throw item - 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 - if (panic) throw item - 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) => { - const built = buildString(i, buildState) - if (panic && x === data.length - 1) return `(typeof ((prev = ${built}) || 0).error !== 'undefined' || Number.isNaN(prev) ? (() => { throw prev.error })() : prev)` - if (Array.isArray(i) || !i || typeof i !== 'object' || x === data.length - 1) return built - return `${funcCall}(` + built + ')' - }).join(' ?? ')})` - } - return `(${buildString(data, buildState)}).reduce((a,b) => ${funcCall}(a) ?? b, null)` - }, - lazy: true - } -} - function createArrayIterativeMethod (name, useTruthy = false) { return { deterministic: (data, buildState) => { @@ -939,15 +927,15 @@ defaultMethods.if.compile = function (data, buildState) { * 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)})` + if (Array.isArray(i)) return 'precoerceNumber(NaN)' + if (typeof i === 'string' || typeof i === 'number' || typeof i === 'boolean') return `precoerceNumber(+${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 => numberCoercion(i, buildState)).join(' + ')})` - if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `(+${buildString(data, buildState)})` + if (typeof data === 'string' || typeof data === 'number' || typeof data === 'boolean') return `precoerceNumber(+${buildString(data, buildState)})` return buildState.compile`(Array.isArray(prev = ${data}) ? prev.reduce((a,b) => (+a)+(+precoerceNumber(b)), 0) : +precoerceNumber(prev))` } diff --git a/suites/multiply.json b/suites/multiply.json index 2308624..7e66f5b 100644 --- a/suites/multiply.json +++ b/suites/multiply.json @@ -157,6 +157,12 @@ "error": true, "data": null }, + { + "description": "Multiply with a single string produces NaN", + "rule": { "*": ["Hey"] }, + "error": true, + "data": null + }, { "description": "Multiply with Array produces NaN", "rule": { "*": [[1], 1] }, diff --git a/utilities/downgrade.js b/utilities/downgrade.js index bee1231..ef3d02d 100644 --- a/utilities/downgrade.js +++ b/utilities/downgrade.js @@ -1,19 +1,9 @@ - -/** - * 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 -} - /** * Used to precoerce a data value to a number, for the purposes of coalescing. * @param {any} item */ export function precoerceNumber (item) { + if (Number.isNaN(item)) throw new Error('NaN') if (!item) return item if (typeof item === 'object') throw new Error('NaN') return item From fdb4f5cd0b68cad1a890bf1d2a59b8db923fb37e Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 15:37:03 -0600 Subject: [PATCH 6/8] `throw NaN` which I know is weird. I can revert this later on --- compatible.test.js | 4 ++-- defaultMethods.js | 42 +++++++++++++++++++++--------------------- utilities/downgrade.js | 4 ++-- 3 files changed, 25 insertions(+), 25 deletions(-) diff --git a/compatible.test.js b/compatible.test.js index 7e12f7b..8573934 100644 --- a/compatible.test.js +++ b/compatible.test.js @@ -56,7 +56,7 @@ describe('All of the compatible tests', () => { if (Array.isArray(result)) result = result.map(i => (i || 0).toNumber ? Number(i) : i) expect(correction(result)).toStrictEqual(testCase.result) } catch (err) { - if (err.message.includes('expect')) throw err + if (err.message && err.message.includes('expect')) throw err expect(testCase.error).toStrictEqual(true) } }) @@ -71,7 +71,7 @@ describe('All of the compatible tests', () => { if (Array.isArray(result)) result = result.map(i => i.toNumber ? Number(i) : i) expect(correction(result)).toStrictEqual(testCase.result) } catch (err) { - if (err.message.includes('expect')) throw err + if (err.message && err.message.includes('expect')) throw err expect(testCase.error).toStrictEqual(true) } }) diff --git a/defaultMethods.js b/defaultMethods.js index 23ecde0..55a5832 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -59,32 +59,32 @@ const defaultMethods = { if (typeof data === 'string') return precoerceNumber(+data) if (typeof data === 'number') return precoerceNumber(+data) if (typeof data === 'boolean') return precoerceNumber(+data) - if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') + if (typeof data === 'object' && !Array.isArray(data)) throw NaN let res = 0 for (let i = 0; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw new Error('NaN') + if (data[i] && typeof data[i] === 'object') throw NaN res += +data[i] } - if (Number.isNaN(res)) throw new Error('NaN') + if (Number.isNaN(res)) throw NaN return res }, '*': (data) => { let res = 1 for (let i = 0; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw new Error('NaN') + if (data[i] && typeof data[i] === 'object') throw NaN res *= +data[i] } - if (Number.isNaN(res)) throw new Error('NaN') + if (Number.isNaN(res)) throw NaN return res }, '/': (data) => { - if (data[0] && typeof data[0] === 'object') throw new Error('NaN') + if (data[0] && typeof data[0] === 'object') throw NaN let res = +data[0] for (let i = 1; i < data.length; i++) { - if ((data[i] && typeof data[i] === 'object') || !data[i]) throw new Error('NaN') + if ((data[i] && typeof data[i] === 'object') || !data[i]) throw NaN res /= +data[i] } - if (Number.isNaN(res)) throw new Error('NaN') + if (Number.isNaN(res)) throw NaN return res }, '-': (data) => { @@ -92,25 +92,25 @@ const defaultMethods = { if (typeof data === 'string') return precoerceNumber(-data) if (typeof data === 'number') return precoerceNumber(-data) if (typeof data === 'boolean') return precoerceNumber(-data) - if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') - if (data[0] && typeof data[0] === 'object') throw new Error('NaN') + if (typeof data === 'object' && !Array.isArray(data)) throw NaN + if (data[0] && typeof data[0] === 'object') throw NaN if (data.length === 1) return -data[0] let res = data[0] for (let i = 1; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw new Error('NaN') + if (data[i] && typeof data[i] === 'object') throw NaN res -= +data[i] } - if (Number.isNaN(res)) throw new Error('NaN') + if (Number.isNaN(res)) throw NaN return res }, '%': (data) => { - if (data[0] && typeof data[0] === 'object') throw new Error('NaN') + if (data[0] && typeof data[0] === 'object') throw NaN let res = +data[0] for (let i = 1; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw new Error('NaN') + if (data[i] && typeof data[i] === 'object') throw NaN res %= +data[i] } - if (Number.isNaN(res)) throw new Error('NaN') + if (Number.isNaN(res)) throw NaN return res }, error: (type) => { @@ -347,8 +347,8 @@ const defaultMethods = { else item = executeInLoop ? engine.run(arr[i], _1, { above: _2 }) : arr[i] return item } catch (e) { - // Do nothing - lastError = e + if (Number.isNaN(e)) lastError = { message: 'NaN' } + else lastError = e } } @@ -368,8 +368,8 @@ const defaultMethods = { else item = executeInLoop ? await engine.run(arr[i], _1, { above: _2 }) : arr[i] return item } catch (e) { - // Do nothing - lastError = e + if (Number.isNaN(e)) lastError = { message: 'NaN' } + else lastError = e } } @@ -962,11 +962,11 @@ defaultMethods['/'].compile = function (data, buildState) { if (Array.isArray(data)) { return `(${data.map((i, x) => { let res = numberCoercion(i, buildState) - if (x) res = `(${res}|| (() => { throw new Error('NaN') })() )` + if (x) res = `(${res}|| (() => { throw NaN })() )` return res }).join(' / ')})` } - return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw new Error('NaN') })() ))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw NaN })() ))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) { diff --git a/utilities/downgrade.js b/utilities/downgrade.js index ef3d02d..0d9e835 100644 --- a/utilities/downgrade.js +++ b/utilities/downgrade.js @@ -3,8 +3,8 @@ * @param {any} item */ export function precoerceNumber (item) { - if (Number.isNaN(item)) throw new Error('NaN') + if (Number.isNaN(item)) throw NaN if (!item) return item - if (typeof item === 'object') throw new Error('NaN') + if (typeof item === 'object') throw NaN return item } From c55794dafa9b87fb973e6365fdbf9ff08d463709 Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 15:38:25 -0600 Subject: [PATCH 7/8] Actually, I'll switch with the new Error, I just want to make sure I support `throw NaN` just in case... --- defaultMethods.js | 34 +++++++++++++++++----------------- utilities/downgrade.js | 4 ++-- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index 55a5832..18c4241 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -59,32 +59,32 @@ const defaultMethods = { if (typeof data === 'string') return precoerceNumber(+data) if (typeof data === 'number') return precoerceNumber(+data) if (typeof data === 'boolean') return precoerceNumber(+data) - if (typeof data === 'object' && !Array.isArray(data)) throw NaN + if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') let res = 0 for (let i = 0; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res += +data[i] } - if (Number.isNaN(res)) throw NaN + if (Number.isNaN(res)) throw new Error('NaN') return res }, '*': (data) => { let res = 1 for (let i = 0; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res *= +data[i] } - if (Number.isNaN(res)) throw NaN + if (Number.isNaN(res)) throw new Error('NaN') return res }, '/': (data) => { - if (data[0] && typeof data[0] === 'object') throw NaN + if (data[0] && typeof data[0] === 'object') throw new Error('NaN') let res = +data[0] for (let i = 1; i < data.length; i++) { - if ((data[i] && typeof data[i] === 'object') || !data[i]) throw NaN + if ((data[i] && typeof data[i] === 'object') || !data[i]) throw new Error('NaN') res /= +data[i] } - if (Number.isNaN(res)) throw NaN + if (Number.isNaN(res)) throw new Error('NaN') return res }, '-': (data) => { @@ -92,25 +92,25 @@ const defaultMethods = { if (typeof data === 'string') return precoerceNumber(-data) if (typeof data === 'number') return precoerceNumber(-data) if (typeof data === 'boolean') return precoerceNumber(-data) - if (typeof data === 'object' && !Array.isArray(data)) throw NaN - if (data[0] && typeof data[0] === 'object') throw NaN + if (typeof data === 'object' && !Array.isArray(data)) throw new Error('NaN') + if (data[0] && typeof data[0] === 'object') throw new Error('NaN') if (data.length === 1) return -data[0] let res = data[0] for (let i = 1; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res -= +data[i] } - if (Number.isNaN(res)) throw NaN + if (Number.isNaN(res)) throw new Error('NaN') return res }, '%': (data) => { - if (data[0] && typeof data[0] === 'object') throw NaN + if (data[0] && typeof data[0] === 'object') throw new Error('NaN') let res = +data[0] for (let i = 1; i < data.length; i++) { - if (data[i] && typeof data[i] === 'object') throw NaN + if (data[i] && typeof data[i] === 'object') throw new Error('NaN') res %= +data[i] } - if (Number.isNaN(res)) throw NaN + if (Number.isNaN(res)) throw new Error('NaN') return res }, error: (type) => { @@ -962,11 +962,11 @@ defaultMethods['/'].compile = function (data, buildState) { if (Array.isArray(data)) { return `(${data.map((i, x) => { let res = numberCoercion(i, buildState) - if (x) res = `(${res}|| (() => { throw NaN })() )` + if (x) res = `(${res}|| (() => { throw new Error('NaN') })() )` return res }).join(' / ')})` } - return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw NaN })() ))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw new Error('NaN') })() ))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) { diff --git a/utilities/downgrade.js b/utilities/downgrade.js index 0d9e835..ef3d02d 100644 --- a/utilities/downgrade.js +++ b/utilities/downgrade.js @@ -3,8 +3,8 @@ * @param {any} item */ export function precoerceNumber (item) { - if (Number.isNaN(item)) throw NaN + if (Number.isNaN(item)) throw new Error('NaN') if (!item) return item - if (typeof item === 'object') throw NaN + if (typeof item === 'object') throw new Error('NaN') return item } From bfe8e754a613d82ce002f1ff7aaf1dc4cb88c88f Mon Sep 17 00:00:00 2001 From: Jesse Mitchell Date: Mon, 20 Jan 2025 15:54:39 -0600 Subject: [PATCH 8/8] Minor optimizations and remove awkward lambda --- defaultMethods.js | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/defaultMethods.js b/defaultMethods.js index 18c4241..e842901 100644 --- a/defaultMethods.js +++ b/defaultMethods.js @@ -928,8 +928,17 @@ defaultMethods.if.compile = function (data, buildState) { */ function numberCoercion (i, buildState) { if (Array.isArray(i)) return 'precoerceNumber(NaN)' - if (typeof i === 'string' || typeof i === 'number' || typeof i === 'boolean') return `precoerceNumber(+${buildString(i, buildState)})` - return `(+precoerceNumber(${buildString(i, buildState)}))` + + if (typeof i === 'number' || typeof i === 'boolean') return '+' + buildString(i, buildState) + if (typeof i === 'string') return '+' + precoerceNumber(+i) + + // check if it's already a number once built + const f = buildString(i, buildState) + + // regex match + if (/^-?\d+(\.\d*)?$/.test(f)) return '+' + f + + return `(+precoerceNumber(${f}))` } // @ts-ignore Allow custom attribute @@ -962,11 +971,11 @@ defaultMethods['/'].compile = function (data, buildState) { if (Array.isArray(data)) { return `(${data.map((i, x) => { let res = numberCoercion(i, buildState) - if (x) res = `(${res}|| (() => { throw new Error('NaN') })() )` + if (x) res = `precoerceNumber(${res} || NaN)` return res }).join(' / ')})` } - return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b) || (() => { throw new Error('NaN') })() ))` + return `(${buildString(data, buildState)}).reduce((a,b) => (+precoerceNumber(a))/(+precoerceNumber(b || NaN)))` } // @ts-ignore Allow custom attribute defaultMethods['*'].compile = function (data, buildState) {