Skip to content

Commit

Permalink
feat: lazy evaluation of and, or, &, | (#3101, #3090)
Browse files Browse the repository at this point in the history
* If fn has rawArgs set, pass unevaluated args

* Add shared helper function for evaluating truthiness

* Add and & or transform functions for lazy evaluation

* Add lazy evaluation of bitwise & and | operators

* Add unit tests for lazy evaluation

* Add lazy evaluation note to docs

* Move documentation to Syntax page

* Replace `testCondition()` with test evaluation
of logical function itself

* Use `isCollection()` to simplify bitwise transform functions

* fix: do not copy scope in raw OperatorNode, test lazy operators scope

* fix: linting issues

---------

Co-authored-by: Brooks Smith <[email protected]>
  • Loading branch information
josdejong and smith120bh authored Dec 8, 2023
1 parent 424735a commit 7e9ff61
Show file tree
Hide file tree
Showing 8 changed files with 168 additions and 5 deletions.
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
9 changes: 8 additions & 1 deletion src/expression/node/OperatorNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,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, 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'
export { createOrTransform } from './expression/transform/or.transform.js'
export { createBitAndTransform } from './expression/transform/bitAnd.transform.js'
export { createBitOrTransform } from './expression/transform/bitOr.transform.js'
40 changes: 40 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,16 @@ 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)
assert.strictEqual(parseAndEval('false & undefined'), 0)
assert.throws(function () { parseAndEval('true & undefined') }, TypeError)
})

it('should parse bitwise and & lazily', function () {
const scope = {}
parseAndEval('(a=false) & (b=true)', scope)
assert.deepStrictEqual(scope, { a: false })
})

it('should parse bitwise xor ^|', function () {
Expand All @@ -1483,6 +1493,16 @@ 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 or | lazily', function () {
const scope = {}
parseAndEval('(a=true) | (b=true)', scope)
assert.deepStrictEqual(scope, { a: true })
})

it('should parse bitwise left shift <<', function () {
Expand All @@ -1506,6 +1526,16 @@ 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 and lazily', function () {
const scope = {}
parseAndEval('(a=false) and (b=true)', scope)
assert.deepStrictEqual(scope, { a: false })
})

it('should parse logical xor', function () {
Expand All @@ -1524,6 +1554,16 @@ 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 or lazily', function () {
const scope = {}
parseAndEval('(a=true) or (b=true)', scope)
assert.deepStrictEqual(scope, { a: true })
})

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

0 comments on commit 7e9ff61

Please sign in to comment.