Skip to content

Create a branch that uses hard exceptions instead #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jan 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions compatible.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 && err.message.includes('expect')) throw err
expect(testCase.error).toStrictEqual(true)
}
})
Expand All @@ -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 && err.message.includes('expect')) throw err
expect(testCase.error).toStrictEqual(true)
}
})
Expand Down
6 changes: 3 additions & 3 deletions compiler.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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')
})
Expand Down
224 changes: 131 additions & 93 deletions defaultMethods.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -56,68 +56,65 @@ 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 === 'object' && !Array.isArray(data)) return Number.NaN
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') return Number.NaN
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) => {
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]
}
if (Number.isNaN(res)) throw new Error('NaN')
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]
}
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 === 'object' && !Array.isArray(data)) return Number.NaN
if (data[0] && typeof data[0] === 'object') return Number.NaN
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]
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]
}
if (Number.isNaN(res)) throw new Error('NaN')
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]
}
if (Number.isNaN(res)) throw new Error('NaN')
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),
Expand Down Expand Up @@ -289,8 +286,98 @@ const defaultMethods = {
},
lazy: true
},
'??': defineCoalesce(),
try: defineCoalesce(downgrade, true),
'??': {
[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) => {
// 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 {
// 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) {
if (Number.isNaN(e)) lastError = { message: 'NaN' }
else 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 {
// 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) {
if (Number.isNaN(e)) lastError = { message: 'NaN' }
else lastError = e
}
}

throw lastError
},
deterministic: (data, buildState) => isDeterministic(data, buildState.engine, buildState),
lazy: true
},
and: {
[Sync]: (data, buildState) => isSyncDeep(data, buildState.engine, buildState),
method: (arr, _1, _2, engine) => {
Expand Down Expand Up @@ -712,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) => {
Expand Down Expand Up @@ -898,15 +927,24 @@ 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)})`
return `(+precoerceNumber(${buildString(i, buildState)}))`
if (Array.isArray(i)) return 'precoerceNumber(NaN)'

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
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))`
}

Expand All @@ -933,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}||NaN)`
if (x) res = `precoerceNumber(${res} || NaN)`
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 || NaN)))`
}
// @ts-ignore Allow custom attribute
defaultMethods['*'].compile = function (data, buildState) {
Expand All @@ -964,7 +1002,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,
Expand Down
4 changes: 2 additions & 2 deletions general.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading