Skip to content

Commit 7e9ff61

Browse files
feat: lazy evaluation of and, or, &, | (#3101, #3090)
* 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]>
1 parent 424735a commit 7e9ff61

File tree

8 files changed

+168
-5
lines changed

8 files changed

+168
-5
lines changed

docs/expressions/syntax.md

+11-4
Original file line numberDiff line numberDiff line change
@@ -121,18 +121,25 @@ See section below | Implicit multiplication
121121
`to`, `in` | Unit conversion
122122
`<<`, `>>`, `>>>` | Bitwise left shift, bitwise right arithmetic shift, bitwise right logical shift
123123
`==`, `!=`, `<`, `>`, `<=`, `>=` | Relational
124-
`&` | Bitwise and
124+
`&` | Bitwise and (lazily evaluated)
125125
<code>^&#124;</code> | Bitwise xor
126-
<code>&#124;</code> | Bitwise or
127-
`and` | Logical and
126+
<code>&#124;</code> | Bitwise or (lazily evaluated)
127+
`and` | Logical and (lazily evaluated)
128128
`xor` | Logical xor
129-
`or` | Logical or
129+
`or` | Logical or (lazily evaluated)
130130
`?`, `:` | Conditional expression
131131
`=` | Assignment
132132
`,` | Parameter and column separator
133133
`;` | Row separator
134134
`\n`, `;` | Statement separators
135135

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

137144
## Functions
138145

src/expression/node/OperatorNode.js

+8-1
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,14 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({
304304
return arg._compile(math, argNames)
305305
})
306306

307-
if (evalArgs.length === 1) {
307+
if (typeof fn === 'function' && fn.rawArgs === true) {
308+
// pass unevaluated parameters (nodes) to the function
309+
// "raw" evaluation
310+
const rawArgs = this.args
311+
return function evalOperatorNode (scope, args, context) {
312+
return fn(rawArgs, math, scope)
313+
}
314+
} else if (evalArgs.length === 1) {
308315
const evalArg0 = evalArgs[0]
309316
return function evalOperatorNode (scope, args, context) {
310317
return fn(evalArg0(scope, args, context))
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createAnd } from '../../function/logical/and.js'
2+
import { factory } from '../../utils/factory.js'
3+
import { isCollection } from '../../utils/is.js'
4+
5+
const name = 'and'
6+
const dependencies = ['typed', 'matrix', 'zeros', 'add', 'equalScalar', 'not', 'concat']
7+
8+
export const createAndTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, zeros, not, concat }) => {
9+
const and = createAnd({ typed, matrix, equalScalar, zeros, not, concat })
10+
11+
function andTransform (args, math, scope) {
12+
const condition1 = args[0].compile().evaluate(scope)
13+
if (!isCollection(condition1) && !and(condition1, true)) {
14+
return false
15+
}
16+
const condition2 = args[1].compile().evaluate(scope)
17+
return and(condition1, condition2)
18+
}
19+
20+
andTransform.rawArgs = true
21+
22+
return andTransform
23+
}, { isTransformFunction: true })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createBitAnd } from '../../function/bitwise/bitAnd.js'
2+
import { factory } from '../../utils/factory.js'
3+
import { isCollection } from '../../utils/is.js'
4+
5+
const name = 'bitAnd'
6+
const dependencies = ['typed', 'matrix', 'zeros', 'add', 'equalScalar', 'not', 'concat']
7+
8+
export const createBitAndTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, zeros, not, concat }) => {
9+
const bitAnd = createBitAnd({ typed, matrix, equalScalar, zeros, not, concat })
10+
11+
function bitAndTransform (args, math, scope) {
12+
const condition1 = args[0].compile().evaluate(scope)
13+
if (!isCollection(condition1)) {
14+
if (isNaN(condition1)) {
15+
return NaN
16+
}
17+
if (condition1 === 0 || condition1 === false) {
18+
return 0
19+
}
20+
}
21+
const condition2 = args[1].compile().evaluate(scope)
22+
return bitAnd(condition1, condition2)
23+
}
24+
25+
bitAndTransform.rawArgs = true
26+
27+
return bitAndTransform
28+
}, { isTransformFunction: true })
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { createBitOr } from '../../function/bitwise/bitOr.js'
2+
import { factory } from '../../utils/factory.js'
3+
import { isCollection } from '../../utils/is.js'
4+
5+
const name = 'bitOr'
6+
const dependencies = ['typed', 'matrix', 'equalScalar', 'DenseMatrix', 'concat']
7+
8+
export const createBitOrTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, DenseMatrix, concat }) => {
9+
const bitOr = createBitOr({ typed, matrix, equalScalar, DenseMatrix, concat })
10+
11+
function bitOrTransform (args, math, scope) {
12+
const condition1 = args[0].compile().evaluate(scope)
13+
if (!isCollection(condition1)) {
14+
if (isNaN(condition1)) {
15+
return NaN
16+
}
17+
if (condition1 === (-1)) {
18+
return -1
19+
}
20+
if (condition1 === true) {
21+
return 1
22+
}
23+
}
24+
const condition2 = args[1].compile().evaluate(scope)
25+
return bitOr(condition1, condition2)
26+
}
27+
28+
bitOrTransform.rawArgs = true
29+
30+
return bitOrTransform
31+
}, { isTransformFunction: true })
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { createOr } from '../../function/logical/or.js'
2+
import { factory } from '../../utils/factory.js'
3+
import { isCollection } from '../../utils/is.js'
4+
5+
const name = 'or'
6+
const dependencies = ['typed', 'matrix', 'equalScalar', 'DenseMatrix', 'concat']
7+
8+
export const createOrTransform = /* #__PURE__ */ factory(name, dependencies, ({ typed, matrix, equalScalar, DenseMatrix, concat }) => {
9+
const or = createOr({ typed, matrix, equalScalar, DenseMatrix, concat })
10+
11+
function orTransform (args, math, scope) {
12+
const condition1 = args[0].compile().evaluate(scope)
13+
if (!isCollection(condition1) && or(condition1, false)) {
14+
return true
15+
}
16+
const condition2 = args[1].compile().evaluate(scope)
17+
return or(condition1, condition2)
18+
}
19+
20+
orTransform.rawArgs = true
21+
22+
return orTransform
23+
}, { isTransformFunction: true })

src/factoriesAny.js

+4
Original file line numberDiff line numberDiff line change
@@ -359,3 +359,7 @@ export { createQuantileSeqTransform } from './expression/transform/quantileSeq.t
359359
export { createCumSumTransform } from './expression/transform/cumsum.transform.js'
360360
export { createVarianceTransform } from './expression/transform/variance.transform.js'
361361
export { createPrintTransform } from './expression/transform/print.transform.js'
362+
export { createAndTransform } from './expression/transform/and.transform.js'
363+
export { createOrTransform } from './expression/transform/or.transform.js'
364+
export { createBitAndTransform } from './expression/transform/bitAnd.transform.js'
365+
export { createBitOrTransform } from './expression/transform/bitOr.transform.js'

test/unit-tests/expression/parse.test.js

+40
Original file line numberDiff line numberDiff line change
@@ -1465,6 +1465,16 @@ describe('parse', function () {
14651465
assert.strictEqual(parseAndEval('true & false'), 0)
14661466
assert.strictEqual(parseAndEval('false & true'), 0)
14671467
assert.strictEqual(parseAndEval('false & false'), 0)
1468+
1469+
assert.strictEqual(parseAndEval('0 & undefined'), 0)
1470+
assert.strictEqual(parseAndEval('false & undefined'), 0)
1471+
assert.throws(function () { parseAndEval('true & undefined') }, TypeError)
1472+
})
1473+
1474+
it('should parse bitwise and & lazily', function () {
1475+
const scope = {}
1476+
parseAndEval('(a=false) & (b=true)', scope)
1477+
assert.deepStrictEqual(scope, { a: false })
14681478
})
14691479

14701480
it('should parse bitwise xor ^|', function () {
@@ -1483,6 +1493,16 @@ describe('parse', function () {
14831493
assert.strictEqual(parseAndEval('true | false'), 1)
14841494
assert.strictEqual(parseAndEval('false | true'), 1)
14851495
assert.strictEqual(parseAndEval('false | false'), 0)
1496+
1497+
assert.strictEqual(parseAndEval('-1 | undefined'), -1)
1498+
assert.strictEqual(parseAndEval('true | undefined'), 1)
1499+
assert.throws(function () { parseAndEval('false | undefined') }, TypeError)
1500+
})
1501+
1502+
it('should parse bitwise or | lazily', function () {
1503+
const scope = {}
1504+
parseAndEval('(a=true) | (b=true)', scope)
1505+
assert.deepStrictEqual(scope, { a: true })
14861506
})
14871507

14881508
it('should parse bitwise left shift <<', function () {
@@ -1506,6 +1526,16 @@ describe('parse', function () {
15061526
assert.strictEqual(parseAndEval('true and false'), false)
15071527
assert.strictEqual(parseAndEval('false and true'), false)
15081528
assert.strictEqual(parseAndEval('false and false'), false)
1529+
1530+
assert.strictEqual(parseAndEval('0 and undefined'), false)
1531+
assert.strictEqual(parseAndEval('false and undefined'), false)
1532+
assert.throws(function () { parseAndEval('true and undefined') }, TypeError)
1533+
})
1534+
1535+
it('should parse logical and lazily', function () {
1536+
const scope = {}
1537+
parseAndEval('(a=false) and (b=true)', scope)
1538+
assert.deepStrictEqual(scope, { a: false })
15091539
})
15101540

15111541
it('should parse logical xor', function () {
@@ -1524,6 +1554,16 @@ describe('parse', function () {
15241554
assert.strictEqual(parseAndEval('true or false'), true)
15251555
assert.strictEqual(parseAndEval('false or true'), true)
15261556
assert.strictEqual(parseAndEval('false or false'), false)
1557+
1558+
assert.strictEqual(parseAndEval('2 or undefined'), true)
1559+
assert.strictEqual(parseAndEval('true or undefined'), true)
1560+
assert.throws(function () { parseAndEval('false or undefined') }, TypeError)
1561+
})
1562+
1563+
it('should parse logical or lazily', function () {
1564+
const scope = {}
1565+
parseAndEval('(a=true) or (b=true)', scope)
1566+
assert.deepStrictEqual(scope, { a: true })
15271567
})
15281568

15291569
it('should parse logical not', function () {

0 commit comments

Comments
 (0)