Skip to content

Commit

Permalink
Merge branch 'develop' into determinant
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed Feb 14, 2024
2 parents 0fa448e + cee9deb commit 0336e7a
Show file tree
Hide file tree
Showing 13 changed files with 571 additions and 150 deletions.
8 changes: 8 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# History


# 2024-02-08, 12.3.2

- Improved the performance of custom defined functions in the expression
parser (#3150).
- Fix: #3143 cannot use `and` and `or` inside a function definition.
Regression since `v12.1.0` (#3150).


# 2024-02-01, 12.3.1

- Improved the typings of the arguments of `ArrayNode`, `FunctionNode`,
Expand Down
9 changes: 5 additions & 4 deletions docs/expressions/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,16 +107,17 @@ allowing the function to process the arguments in a customized way. Raw
functions are called as:

```
rawFunction(args: Node[], math: Object, scope: Object)
rawFunction(args: Node[], math: Object, scope: Map)
```

Where :

- `args` is an Array with nodes of the parsed arguments.
- `math` is the math namespace against which the expression was compiled.
- `scope` is a shallow _copy_ of the `scope` object provided when evaluating
the expression, optionally extended with nested variables like a function
parameter `x` of in a custom defined function like `f(x) = x^2`.
- `scope` is a `Map` containing the variables defined in the scope passed
via `evaluate(scope)`. In case of using a custom defined function like
`f(x) = rawFunction(x) ^ 2`, the scope passed to `rawFunction` also contains
the current value of parameter `x`.

Raw functions must be imported in the `math` namespace, as they need to be
processed at compile time. They are not supported when passed via a scope
Expand Down
311 changes: 189 additions & 122 deletions package-lock.json

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mathjs",
"version": "12.3.1",
"version": "12.3.2",
"description": "Math.js is an extensive math library for JavaScript and Node.js. It features a flexible expression parser with support for symbolic computation, comes with a large set of built-in functions and constants, and offers an integrated solution to work with different data types like numbers, big numbers, complex numbers, fractions, units, and matrices.",
"author": "Jos de Jong <wjosdejong@gmail.com> (https://github.com/josdejong)",
"homepage": "https://mathjs.org",
Expand Down Expand Up @@ -43,8 +43,8 @@
"@babel/register": "7.23.7",
"@types/assert": "1.5.10",
"@types/mocha": "10.0.6",
"@typescript-eslint/eslint-plugin": "6.20.0",
"@typescript-eslint/parser": "6.20.0",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"assert": "2.1.0",
"babel-loader": "9.1.3",
"benchmark": "2.1.4",
Expand Down Expand Up @@ -74,7 +74,7 @@
"karma-firefox-launcher": "2.1.2",
"karma-mocha": "2.0.1",
"karma-mocha-reporter": "2.2.5",
"karma-webpack": "5.0.0",
"karma-webpack": "5.0.1",
"mkdirp": "3.0.1",
"mocha": "10.2.0",
"mocha-junit-reporter": "2.2.1",
Expand All @@ -85,12 +85,12 @@
"ndarray-pack": "1.2.1",
"numericjs": "1.2.6",
"pad-right": "0.2.2",
"prettier": "3.2.4",
"prettier": "3.2.5",
"process": "0.11.10",
"sylvester": "0.0.21",
"ts-node": "10.9.2",
"typescript": "5.3.3",
"webpack": "5.90.0",
"webpack": "5.90.1",
"zeros": "1.0.0"
},
"type": "module",
Expand Down
3 changes: 2 additions & 1 deletion src/expression/node/OperatorNode.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isNode, isConstantNode, isOperatorNode, isParenthesisNode } from '../../utils/is.js'
import { map } from '../../utils/array.js'
import { createSubScope } from '../../utils/scope.js'
import { escape } from '../../utils/string.js'
import { getSafeProperty, isSafeMethod } from '../../utils/customs.js'
import { getAssociativity, getPrecedence, isAssociativeWith, properties } from '../operators.js'
Expand Down Expand Up @@ -309,7 +310,7 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({
// "raw" evaluation
const rawArgs = this.args
return function evalOperatorNode (scope, args, context) {
return fn(rawArgs, math, scope)
return fn(rawArgs, math, createSubScope(scope, args))
}
} else if (evalArgs.length === 1) {
const evalArg0 = evalArgs[0]
Expand Down
9 changes: 5 additions & 4 deletions src/expression/transform/utils/compileInlineExpression.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { isSymbolNode } from '../../../utils/is.js'
import { createSubScope } from '../../../utils/scope.js'
import { PartitionedMap } from '../../../utils/map.js'

/**
* Compile an inline expression like "x > 0"
* @param {Node} expression
* @param {Object} math
* @param {Object} scope
* @param {Map} scope
* @return {function} Returns a function with one argument which fills in the
* undefined variable (like "x") and evaluates the expression
*/
Expand All @@ -23,10 +23,11 @@ export function compileInlineExpression (expression, math, scope) {

// create a test function for this equation
const name = symbol.name // variable name
const subScope = createSubScope(scope)
const argsScope = new Map()
const subScope = new PartitionedMap(scope, argsScope, new Set([name]))
const eq = expression.compile()
return function inlineExpression (x) {
subScope.set(name, x)
argsScope.set(name, x)
return eq.evaluate(subScope)
}
}
121 changes: 120 additions & 1 deletion src/utils/map.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export class ObjectWrappingMap {
}

keys () {
return Object.keys(this.wrappedObject)
return Object.keys(this.wrappedObject).values()
}

get (key) {
Expand All @@ -30,6 +30,125 @@ export class ObjectWrappingMap {
has (key) {
return hasSafeProperty(this.wrappedObject, key)
}

entries () {
return mapIterator(this.keys(), key => [key, this.get(key)])
}

forEach (callback) {
for (const key of this.keys()) {
callback(this.get(key), key, this)
}
}

delete (key) {
delete this.wrappedObject[key]
}

clear () {
for (const key of this.keys()) {
this.delete(key)
}
}

get size () {
return Object.keys(this.wrappedObject).length
}
}

/**
* Create a map with two partitions: a and b.
* The set with bKeys determines which keys/values are read/written to map b,
* all other values are read/written to map a
*
* For example:
*
* const a = new Map()
* const b = new Map()
* const p = new PartitionedMap(a, b, new Set(['x', 'y']))
*
* In this case, values `x` and `y` are read/written to map `b`,
* all other values are read/written to map `a`.
*/
export class PartitionedMap {
/**
* @param {Map} a
* @param {Map} b
* @param {Set} bKeys
*/
constructor (a, b, bKeys) {
this.a = a
this.b = b
this.bKeys = bKeys
}

get (key) {
return this.bKeys.has(key)
? this.b.get(key)
: this.a.get(key)
}

set (key, value) {
if (this.bKeys.has(key)) {
this.b.set(key, value)
} else {
this.a.set(key, value)
}
return this
}

has (key) {
return this.b.has(key) || this.a.has(key)
}

keys () {
return new Set([
...this.a.keys(),
...this.b.keys()
])[Symbol.iterator]()
}

entries () {
return mapIterator(this.keys(), key => [key, this.get(key)])
}

forEach (callback) {
for (const key of this.keys()) {
callback(this.get(key), key, this)
}
}

delete (key) {
return this.bKeys.has(key)
? this.b.delete(key)
: this.a.delete(key)
}

clear () {
this.a.clear()
this.b.clear()
}

get size () {
return [...this.keys()].length
}
}

/**
* Create a new iterator that maps over the provided iterator, applying a mapping function to each item
*/
function mapIterator (it, callback) {
return {
next: () => {
const n = it.next()
return (n.done)
? n
: {
value: callback(n.value),
done: false
}
}
}
}

/**
Expand Down
18 changes: 9 additions & 9 deletions src/utils/scope.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createEmptyMap, assign } from './map.js'
import { ObjectWrappingMap, PartitionedMap } from './map.js'

/**
* Create a new scope which can access the parent scope,
Expand All @@ -10,13 +10,13 @@ import { createEmptyMap, assign } from './map.js'
* the remaining `args`.
*
* @param {Map} parentScope
* @param {...any} args
* @returns {Map}
* @param {Object} args
* @returns {PartitionedMap}
*/
export function createSubScope (parentScope, ...args) {
if (typeof parentScope.createSubScope === 'function') {
return assign(parentScope.createSubScope(), ...args)
}

return assign(createEmptyMap(), parentScope, ...args)
export function createSubScope (parentScope, args) {
return new PartitionedMap(
parentScope,
new ObjectWrappingMap(args),
new Set(Object.keys(args))
)
}
2 changes: 1 addition & 1 deletion src/version.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export const version = '12.3.1'
export const version = '12.3.2'
// Note: This file is automatically generated when building math.js.
// Changes made in this file will be overwritten.
1 change: 1 addition & 0 deletions test/benchmark/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ require('./roots')
require('./matrix_operations')
require('./prime')
require('./load')
require('./scope_variables.js')
20 changes: 20 additions & 0 deletions test/benchmark/scope_variables.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// test performance of resolving scope variables in the expression parser

const Benchmark = require('benchmark')
const math = require('../..')

const scope = { a: 2, b: 3, c: 4 }
const f = math.evaluate('f(x, y) = a + b + c + x + y', scope)

console.log('f(5, 6) = ' + f(5, 6))

const suite = new Benchmark.Suite()
let res = 0
suite
.add('evaluate f(x, y)', function () {
res = f(-res, res) // make it dynamic, using res as argument
})
.on('cycle', function (event) {
console.log(String(event.target))
})
.run()
32 changes: 32 additions & 0 deletions test/unit-tests/expression/parse.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ describe('parse', function () {
assert.strictEqual(scope.f(3), 9)
})

it('should support variable assignment inside a function definition', function () {
const scope = {}
parse('f(x)=(y=x)*2').compile().evaluate(scope)
assert.strictEqual(scope.f(2), 4)
assert.strictEqual(scope.y, 2)
})

it('should spread a function over multiple lines', function () {
assert.deepStrictEqual(parse('add(\n4\n,\n2\n)').compile().evaluate(), 6)
})
Expand Down Expand Up @@ -1538,6 +1545,23 @@ describe('parse', function () {
assert.deepStrictEqual(scope, { a: false })
})

it('should parse logical and inside a function definition', function () {
const scope = {}
const f = parseAndEval('f(x) = x > 2 and x < 4', scope)
assert.strictEqual(f(1), false)
assert.strictEqual(f(3), true)
assert.strictEqual(f(5), false)
})

it('should use a variable assignment with a rawArgs function inside a function definition', function () {
const scope = {}
const f = parseAndEval('f(x) = (a=false) and (b=true)', scope)
assert.deepStrictEqual(parseAndEval('f(2)', scope), false)
assert.deepStrictEqual(Object.keys(scope), ['f', 'a'])
assert.strictEqual(scope.f, f)
assert.strictEqual(scope.a, false)
})

it('should parse logical xor', function () {
assert.strictEqual(parseAndEval('2 xor 6'), false)
assert.strictEqual(parseAndEval('2 xor 0'), true)
Expand All @@ -1560,6 +1584,14 @@ describe('parse', function () {
assert.throws(function () { parseAndEval('false or undefined') }, TypeError)
})

it('should parse logical or inside a function definition', function () {
const scope = {}
const f = parseAndEval('f(x) = x < 2 or x > 4', scope)
assert.strictEqual(f(1), true)
assert.strictEqual(f(3), false)
assert.strictEqual(f(5), true)
})

it('should parse logical or lazily', function () {
const scope = {}
parseAndEval('(a=true) or (b=true)', scope)
Expand Down
Loading

0 comments on commit 0336e7a

Please sign in to comment.