Skip to content

Commit a4815d5

Browse files
authored
feat: implement serialization support for Parser (fixes #3509) (#3525)
1 parent a7f2da0 commit a4815d5

File tree

5 files changed

+120
-1
lines changed

5 files changed

+120
-1
lines changed

docs/expressions/parsing.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -234,3 +234,31 @@ Some care is taken to mutate the same object that is passed into mathjs, so they
234234
For less reliance on this blacklist, scope can also be a `Map`, which allows mathjs expressions to define variables and functions of any name.
235235

236236
For more, see [examples of custom scopes](../../examples/advanced/custom_scope_objects.js).
237+
238+
## Serialization
239+
240+
All mathjs data types can be serialized. A scope containing variables can therefore be safely serialized too. However, in the expression parser it is possible to define functions, like:
241+
242+
```
243+
f(x) = x^2
244+
```
245+
246+
Such a custom function cannot be serialized on its own, since it may be bound to other variables in the scope.
247+
248+
A [`Parser`](#parser) can safely serialize all variables and functions evaluated via the expression parser:
249+
250+
```js
251+
const parser = math.parser()
252+
253+
// evaluate some expressions
254+
parser.evaluate('w = 2')
255+
parser.evaluate('f(x) = x^w')
256+
parser.evaluate('c = f(3)') // 9
257+
258+
// serialize the parser with its state
259+
const str = JSON.stringify(parser)
260+
261+
// deserialize the parser again
262+
const parser2 = JSON.parse(str, math.reviver)
263+
parser.evaluate('f(4)') // 16
264+
```

src/expression/Parser.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { factory } from '../utils/factory.js'
2+
import { isFunction } from '../utils/is.js'
23
import { createEmptyMap, toObject } from '../utils/map.js'
34

45
const name = 'Parser'
@@ -157,5 +158,42 @@ export const createParserClass = /* #__PURE__ */ factory(name, dependencies, ({
157158
this.scope.clear()
158159
}
159160

161+
Parser.prototype.toJSON = function () {
162+
const json = {
163+
mathjs: 'Parser',
164+
variables: {},
165+
functions: {}
166+
}
167+
168+
for (const [name, value] of this.scope) {
169+
if (isFunction(value)) {
170+
if (!isExpressionFunction(value)) {
171+
throw new Error(`Cannot serialize external function ${name}`)
172+
}
173+
174+
json.functions[name] = `${value.syntax} = ${value.expr}`
175+
} else {
176+
json.variables[name] = value
177+
}
178+
}
179+
180+
return json
181+
}
182+
183+
Parser.fromJSON = function (json) {
184+
const parser = new Parser()
185+
186+
Object.entries(json.variables).forEach(([name, value]) => parser.set(name, value))
187+
Object.entries(json.functions).forEach(([_name, fn]) => parser.evaluate(fn))
188+
189+
return parser
190+
}
191+
160192
return Parser
161193
}, { isClass: true })
194+
195+
function isExpressionFunction (value) {
196+
return typeof value === 'function' &&
197+
typeof value.syntax === 'string' &&
198+
typeof value.expr === 'string'
199+
}

src/expression/node/FunctionAssignmentNode.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,8 @@ export const createFunctionAssignmentNode = /* #__PURE__ */ factory(name, depend
9797
})
9898

9999
// compile the function expression with the child args
100-
const evalExpr = this.expr._compile(math, childArgNames)
100+
const expr = this.expr
101+
const evalExpr = expr._compile(math, childArgNames)
101102
const name = this.name
102103
const params = this.params
103104
const signature = join(this.types, ',')
@@ -116,6 +117,7 @@ export const createFunctionAssignmentNode = /* #__PURE__ */ factory(name, depend
116117
}
117118
const fn = typed(name, signatures)
118119
fn.syntax = syntax
120+
fn.expr = expr.toString()
119121

120122
scope.set(name, fn)
121123

test/unit-tests/json/replacer.test.js

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,36 @@ describe('replacer', function () {
187187
assert.deepStrictEqual(JSON.parse(JSON.stringify(node, replacer)), json)
188188
})
189189

190+
it('should stringify a Parser', function () {
191+
const parser = new math.Parser()
192+
parser.evaluate('a = 42')
193+
parser.evaluate('w = bignumber(2)')
194+
parser.evaluate('f(x) = w * x')
195+
parser.evaluate('c = f(3)')
196+
197+
const json = {
198+
mathjs: 'Parser',
199+
variables: {
200+
a: 42,
201+
c: { mathjs: 'BigNumber', value: '6' },
202+
w: { mathjs: 'BigNumber', value: '2' }
203+
},
204+
functions: {
205+
f: 'f(x) = w * x'
206+
}
207+
}
208+
209+
assert.deepStrictEqual(JSON.parse(JSON.stringify(parser)), json)
210+
assert.deepStrictEqual(JSON.parse(JSON.stringify(parser, replacer)), json)
211+
})
212+
213+
it('should throw when stringifying a Parser containing external functions', function () {
214+
const parser = new math.Parser()
215+
parser.set('f', (x) => 2 * x)
216+
217+
assert.throws(() => JSON.stringify(parser), /Cannot serialize external function f/)
218+
})
219+
190220
it('should stringify Help', function () {
191221
const h = new math.Help({ name: 'foo', description: 'bar' })
192222
const json = '{"mathjs":"Help","name":"foo","description":"bar"}'

test/unit-tests/json/reviver.test.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,25 @@ describe('reviver', function () {
246246
assert.strictEqual(node.type, 'OperatorNode')
247247
assert.strictEqual(node.toString(), '2 + sin(3 x)')
248248
})
249+
250+
it('should parse a stringified Parser', function () {
251+
const json = JSON.stringify({
252+
mathjs: 'Parser',
253+
variables: {
254+
a: 42,
255+
c: { mathjs: 'BigNumber', value: '6' },
256+
w: { mathjs: 'BigNumber', value: '2' }
257+
},
258+
functions: {
259+
f: 'f(x) = w * x'
260+
}
261+
})
262+
263+
const parser = JSON.parse(json, reviver)
264+
265+
assert.deepStrictEqual(parser.get('a'), 42)
266+
assert.deepStrictEqual(parser.get('c'), math.bignumber('6'))
267+
assert.deepStrictEqual(parser.get('w'), math.bignumber('2'))
268+
assert.deepStrictEqual(parser.evaluate('f(4)'), math.bignumber('8'))
269+
})
249270
})

0 commit comments

Comments
 (0)