diff --git a/docs/expressions/syntax.md b/docs/expressions/syntax.md
index 8e8681c823..722f8084ae 100644
--- a/docs/expressions/syntax.md
+++ b/docs/expressions/syntax.md
@@ -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)
^|
| Bitwise xor
-|
| Bitwise or
-`and` | Logical and
+|
| 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
diff --git a/src/expression/node/OperatorNode.js b/src/expression/node/OperatorNode.js
index 21eeeb744c..27efdcfcbb 100644
--- a/src/expression/node/OperatorNode.js
+++ b/src/expression/node/OperatorNode.js
@@ -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))
diff --git a/src/expression/transform/and.transform.js b/src/expression/transform/and.transform.js
new file mode 100644
index 0000000000..4212eecefe
--- /dev/null
+++ b/src/expression/transform/and.transform.js
@@ -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 })
diff --git a/src/expression/transform/bitAnd.transform.js b/src/expression/transform/bitAnd.transform.js
new file mode 100644
index 0000000000..ff9e709261
--- /dev/null
+++ b/src/expression/transform/bitAnd.transform.js
@@ -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 })
diff --git a/src/expression/transform/bitOr.transform.js b/src/expression/transform/bitOr.transform.js
new file mode 100644
index 0000000000..ae04f7de04
--- /dev/null
+++ b/src/expression/transform/bitOr.transform.js
@@ -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 })
diff --git a/src/expression/transform/or.transform.js b/src/expression/transform/or.transform.js
new file mode 100644
index 0000000000..029f19bd4c
--- /dev/null
+++ b/src/expression/transform/or.transform.js
@@ -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 })
diff --git a/src/factoriesAny.js b/src/factoriesAny.js
index 74ab542b7a..e9ae77caa1 100644
--- a/src/factoriesAny.js
+++ b/src/factoriesAny.js
@@ -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'
diff --git a/test/unit-tests/expression/parse.test.js b/test/unit-tests/expression/parse.test.js
index e7d919b828..5e5cfc153e 100644
--- a/test/unit-tests/expression/parse.test.js
+++ b/test/unit-tests/expression/parse.test.js
@@ -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 () {
@@ -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 () {
@@ -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 () {
@@ -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 () {