Skip to content
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

Lazy evaluation of and, or, &, | #3090

Closed
wants to merge 10 commits into from
15 changes: 11 additions & 4 deletions docs/expressions/syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,18 +121,25 @@ See section below | Implicit multiplication
`to`, `in` | Unit conversion
`<<`, `>>`, `>>>` | Bitwise left shift, bitwise right arithmetic shift, bitwise right logical shift
`==`, `!=`, `<`, `>`, `<=`, `>=` | Relational
`&` | Bitwise and
`&` | Bitwise and (lazily evaluated)
<code>^&#124;</code> | Bitwise xor
<code>&#124;</code> | Bitwise or
`and` | Logical and
<code>&#124;</code> | Bitwise or (lazily evaluated)
`and` | Logical and (lazily evaluated)
`xor` | Logical xor
`or` | Logical or
`or` | Logical or (lazily evaluated)
`?`, `:` | Conditional expression
`=` | Assignment
`,` | Parameter and column separator
`;` | Row separator
`\n`, `;` | Statement separators

Lazy evaluation is used where logically possible for bitwise and logical
operators. In the following example, the value of `x` will not even be
evaluated because it cannot effect the final result:
```js
math.evaluate('false and x') // false, no matter what x equals
```


## Functions

Expand Down
10 changes: 9 additions & 1 deletion src/expression/node/OperatorNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getSafeProperty, isSafeMethod } from '../../utils/customs.js'
import { getAssociativity, getPrecedence, isAssociativeWith, properties } from '../operators.js'
import { latexOperators } from '../../utils/latex.js'
import { factory } from '../../utils/factory.js'
import { createSubScope } from '../../utils/scope.js'

const name = 'OperatorNode'
const dependencies = [
Expand Down Expand Up @@ -304,7 +305,14 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({
return arg._compile(math, argNames)
})

if (evalArgs.length === 1) {
if (typeof fn === 'function' && fn.rawArgs === true) {
// pass unevaluated parameters (nodes) to the function
// "raw" evaluation
const rawArgs = this.args
return function evalOperatorNode (scope, args, context) {
return fn(rawArgs, math, createSubScope(scope, args), scope)
}
} else if (evalArgs.length === 1) {
const evalArg0 = evalArgs[0]
return function evalOperatorNode (scope, args, context) {
return fn(evalArg0(scope, args, context))
Expand Down
23 changes: 23 additions & 0 deletions src/expression/transform/and.transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createAnd } from '../../function/logical/and.js'
import { factory } from '../../utils/factory.js'
import { isCollection } from '../../utils/is.js'

const name = 'and'
const dependencies = ['typed', 'matrix', 'zeros', 'add', 'equalScalar', 'not', 'concat']

export const createAndTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, zeros, not, concat }) => {
const and = createAnd({ typed, matrix, equalScalar, zeros, not, concat })

function andTransform (args, math, scope) {
const condition1 = args[0].compile().evaluate(scope)
if (!isCollection(condition1) && !and(condition1, true)) {
return false
}
const condition2 = args[1].compile().evaluate(scope)
return and(condition1, condition2)
}

andTransform.rawArgs = true

return andTransform
}, { isTransformFunction: true })
28 changes: 28 additions & 0 deletions src/expression/transform/bitAnd.transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { createBitAnd } from '../../function/bitwise/bitAnd.js'
import { factory } from '../../utils/factory.js'
import { isCollection } from '../../utils/is.js'

const name = 'bitAnd'
const dependencies = ['typed', 'matrix', 'zeros', 'add', 'equalScalar', 'not', 'concat']

export const createBitAndTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, zeros, not, concat }) => {
const bitAnd = createBitAnd({ typed, matrix, equalScalar, zeros, not, concat })

function bitAndTransform (args, math, scope) {
const condition1 = args[0].compile().evaluate(scope)
if (!isCollection(condition1)) {
if (isNaN(condition1)) {
return NaN
}
if (condition1 === 0 || condition1 === false) {
return 0
}
}
const condition2 = args[1].compile().evaluate(scope)
return bitAnd(condition1, condition2)
}

bitAndTransform.rawArgs = true

return bitAndTransform
}, { isTransformFunction: true })
31 changes: 31 additions & 0 deletions src/expression/transform/bitOr.transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { createBitOr } from '../../function/bitwise/bitOr.js'
import { factory } from '../../utils/factory.js'
import { isCollection } from '../../utils/is.js'

const name = 'bitOr'
const dependencies = ['typed', 'matrix', 'equalScalar', 'DenseMatrix', 'concat']

export const createBitOrTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, DenseMatrix, concat }) => {
const bitOr = createBitOr({ typed, matrix, equalScalar, DenseMatrix, concat })

function bitOrTransform (args, math, scope) {
const condition1 = args[0].compile().evaluate(scope)
if (!isCollection(condition1)) {
if (isNaN(condition1)) {
return NaN
}
if (condition1 === (-1)) {
return -1
}
if (condition1 === true) {
return 1
}
}
const condition2 = args[1].compile().evaluate(scope)
return bitOr(condition1, condition2)
}

bitOrTransform.rawArgs = true

return bitOrTransform
}, { isTransformFunction: true })
23 changes: 23 additions & 0 deletions src/expression/transform/or.transform.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { createOr } from '../../function/logical/or.js'
import { factory } from '../../utils/factory.js'
import { isCollection } from '../../utils/is.js'

const name = 'or'
const dependencies = ['typed', 'matrix', 'equalScalar', 'DenseMatrix', 'concat']

export const createOrTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, DenseMatrix, concat }) => {
const or = createOr({ typed, matrix, equalScalar, DenseMatrix, concat })

function orTransform (args, math, scope) {
const condition1 = args[0].compile().evaluate(scope)
if (!isCollection(condition1) && or(condition1, false)) {
return true
}
const condition2 = args[1].compile().evaluate(scope)
return or(condition1, condition2)
}

orTransform.rawArgs = true

return orTransform
}, { isTransformFunction: true })
4 changes: 4 additions & 0 deletions src/factoriesAny.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,3 +359,7 @@ export { createQuantileSeqTransform } from './expression/transform/quantileSeq.t
export { createCumSumTransform } from './expression/transform/cumsum.transform.js'
export { createVarianceTransform } from './expression/transform/variance.transform.js'
export { createPrintTransform } from './expression/transform/print.transform.js'
export { createAndTransform } from './expression/transform/and.transform.js'
smith120bh marked this conversation as resolved.
Show resolved Hide resolved
export { createOrTransform } from './expression/transform/or.transform.js'
export { createBitAndTransform } from './expression/transform/bitAnd.transform.js'
export { createBitOrTransform } from './expression/transform/bitOr.transform.js'
16 changes: 16 additions & 0 deletions test/unit-tests/expression/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1465,6 +1465,10 @@ describe('parse', function () {
assert.strictEqual(parseAndEval('true & false'), 0)
assert.strictEqual(parseAndEval('false & true'), 0)
assert.strictEqual(parseAndEval('false & false'), 0)

assert.strictEqual(parseAndEval('0 & undefined'), 0)
smith120bh marked this conversation as resolved.
Show resolved Hide resolved
assert.strictEqual(parseAndEval('false & undefined'), 0)
assert.throws(function () { parseAndEval('true & undefined') }, TypeError)
josdejong marked this conversation as resolved.
Show resolved Hide resolved
})

it('should parse bitwise xor ^|', function () {
Expand All @@ -1483,6 +1487,10 @@ describe('parse', function () {
assert.strictEqual(parseAndEval('true | false'), 1)
assert.strictEqual(parseAndEval('false | true'), 1)
assert.strictEqual(parseAndEval('false | false'), 0)

assert.strictEqual(parseAndEval('-1 | undefined'), -1)
assert.strictEqual(parseAndEval('true | undefined'), 1)
assert.throws(function () { parseAndEval('false | undefined') }, TypeError)
})

it('should parse bitwise left shift <<', function () {
Expand All @@ -1506,6 +1514,10 @@ describe('parse', function () {
assert.strictEqual(parseAndEval('true and false'), false)
assert.strictEqual(parseAndEval('false and true'), false)
assert.strictEqual(parseAndEval('false and false'), false)

assert.strictEqual(parseAndEval('0 and undefined'), false)
assert.strictEqual(parseAndEval('false and undefined'), false)
assert.throws(function () { parseAndEval('true and undefined') }, TypeError)
})

it('should parse logical xor', function () {
Expand All @@ -1524,6 +1536,10 @@ describe('parse', function () {
assert.strictEqual(parseAndEval('true or false'), true)
assert.strictEqual(parseAndEval('false or true'), true)
assert.strictEqual(parseAndEval('false or false'), false)

assert.strictEqual(parseAndEval('2 or undefined'), true)
assert.strictEqual(parseAndEval('true or undefined'), true)
assert.throws(function () { parseAndEval('false or undefined') }, TypeError)
})

it('should parse logical not', function () {
Expand Down