UNPKG

24 kBJavaScriptView Raw
1import { isConstantNode, isParenthesisNode } from '../../utils/is'
2import { factory } from '../../utils/factory'
3import { createUtil } from './simplify/util'
4import { createSimplifyCore } from './simplify/simplifyCore'
5import { createSimplifyConstant } from './simplify/simplifyConstant'
6import { createResolve } from './simplify/resolve'
7import { hasOwnProperty } from '../../utils/object'
8
9const name = 'simplify'
10const dependencies = [
11 'config',
12 'typed',
13 'parse',
14 'add',
15 'subtract',
16 'multiply',
17 'divide',
18 'pow',
19 'isZero',
20 'equal',
21 '?fraction',
22 '?bignumber',
23 'mathWithTransform',
24 'ConstantNode',
25 'FunctionNode',
26 'OperatorNode',
27 'ParenthesisNode',
28 'SymbolNode'
29]
30
31export const createSimplify = /* #__PURE__ */ factory(name, dependencies, (
32 {
33 config,
34 typed,
35 parse,
36 add,
37 subtract,
38 multiply,
39 divide,
40 pow,
41 isZero,
42 equal,
43 fraction,
44 bignumber,
45 mathWithTransform,
46 ConstantNode,
47 FunctionNode,
48 OperatorNode,
49 ParenthesisNode,
50 SymbolNode
51 }
52) => {
53 const simplifyConstant = createSimplifyConstant({
54 typed,
55 config,
56 mathWithTransform,
57 fraction,
58 bignumber,
59 ConstantNode,
60 OperatorNode,
61 FunctionNode,
62 SymbolNode
63 })
64 const simplifyCore = createSimplifyCore({
65 equal,
66 isZero,
67 add,
68 subtract,
69 multiply,
70 divide,
71 pow,
72 ConstantNode,
73 OperatorNode,
74 FunctionNode,
75 ParenthesisNode
76 })
77 const resolve = createResolve({
78 parse,
79 FunctionNode,
80 OperatorNode,
81 ParenthesisNode
82 })
83
84 const { isCommutative, isAssociative, flatten, unflattenr, unflattenl, createMakeNodeFunction } =
85 createUtil({ FunctionNode, OperatorNode, SymbolNode })
86
87 /**
88 * Simplify an expression tree.
89 *
90 * A list of rules are applied to an expression, repeating over the list until
91 * no further changes are made.
92 * It's possible to pass a custom set of rules to the function as second
93 * argument. A rule can be specified as an object, string, or function:
94 *
95 * const rules = [
96 * { l: 'n1*n3 + n2*n3', r: '(n1+n2)*n3' },
97 * 'n1*n3 + n2*n3 -> (n1+n2)*n3',
98 * function (node) {
99 * // ... return a new node or return the node unchanged
100 * return node
101 * }
102 * ]
103 *
104 * String and object rules consist of a left and right pattern. The left is
105 * used to match against the expression and the right determines what matches
106 * are replaced with. The main difference between a pattern and a normal
107 * expression is that variables starting with the following characters are
108 * interpreted as wildcards:
109 *
110 * - 'n' - matches any Node
111 * - 'c' - matches any ConstantNode
112 * - 'v' - matches any Node that is not a ConstantNode
113 *
114 * The default list of rules is exposed on the function as `simplify.rules`
115 * and can be used as a basis to built a set of custom rules.
116 *
117 * For more details on the theory, see:
118 *
119 * - [Strategies for simplifying math expressions (Stackoverflow)](https://stackoverflow.com/questions/7540227/strategies-for-simplifying-math-expressions)
120 * - [Symbolic computation - Simplification (Wikipedia)](https://en.wikipedia.org/wiki/Symbolic_computation#Simplification)
121 *
122 * An optional `options` argument can be passed as last argument of `simplify`.
123 * There is currently one option available: `exactFractions`, a boolean which
124 * is `true` by default.
125 *
126 * Syntax:
127 *
128 * simplify(expr)
129 * simplify(expr, rules)
130 * simplify(expr, rules)
131 * simplify(expr, rules, scope)
132 * simplify(expr, rules, scope, options)
133 * simplify(expr, scope)
134 * simplify(expr, scope, options)
135 *
136 * Examples:
137 *
138 * math.simplify('2 * 1 * x ^ (2 - 1)') // Node "2 * x"
139 * math.simplify('2 * 3 * x', {x: 4}) // Node "24"
140 * const f = math.parse('2 * 1 * x ^ (2 - 1)')
141 * math.simplify(f) // Node "2 * x"
142 * math.simplify('0.4 * x', {}, {exactFractions: true}) // Node "x * 2 / 5"
143 * math.simplify('0.4 * x', {}, {exactFractions: false}) // Node "0.4 * x"
144 *
145 * See also:
146 *
147 * derivative, parse, evaluate, rationalize
148 *
149 * @param {Node | string} expr
150 * The expression to be simplified
151 * @param {Array<{l:string, r: string} | string | function>} [rules]
152 * Optional list with custom rules
153 * @return {Node} Returns the simplified form of `expr`
154 */
155 const simplify = typed('simplify', {
156 string: function (expr) {
157 return simplify(parse(expr), simplify.rules, {}, {})
158 },
159
160 'string, Object': function (expr, scope) {
161 return simplify(parse(expr), simplify.rules, scope, {})
162 },
163
164 'string, Object, Object': function (expr, scope, options) {
165 return simplify(parse(expr), simplify.rules, scope, options)
166 },
167
168 'string, Array': function (expr, rules) {
169 return simplify(parse(expr), rules, {}, {})
170 },
171
172 'string, Array, Object': function (expr, rules, scope) {
173 return simplify(parse(expr), rules, scope, {})
174 },
175
176 'string, Array, Object, Object': function (expr, rules, scope, options) {
177 return simplify(parse(expr), rules, scope, options)
178 },
179
180 'Node, Object': function (expr, scope) {
181 return simplify(expr, simplify.rules, scope, {})
182 },
183
184 'Node, Object, Object': function (expr, scope, options) {
185 return simplify(expr, simplify.rules, scope, options)
186 },
187
188 Node: function (expr) {
189 return simplify(expr, simplify.rules, {}, {})
190 },
191
192 'Node, Array': function (expr, rules) {
193 return simplify(expr, rules, {}, {})
194 },
195
196 'Node, Array, Object': function (expr, rules, scope) {
197 return simplify(expr, rules, scope, {})
198 },
199
200 'Node, Array, Object, Object': function (expr, rules, scope, options) {
201 rules = _buildRules(rules)
202 let res = resolve(expr, scope)
203 res = removeParens(res)
204 const visited = {}
205 let str = res.toString({ parenthesis: 'all' })
206 while (!visited[str]) {
207 visited[str] = true
208 _lastsym = 0 // counter for placeholder symbols
209 for (let i = 0; i < rules.length; i++) {
210 if (typeof rules[i] === 'function') {
211 res = rules[i](res, options)
212 } else {
213 flatten(res)
214 res = applyRule(res, rules[i])
215 }
216 unflattenl(res) // using left-heavy binary tree here since custom rule functions may expect it
217 }
218 str = res.toString({ parenthesis: 'all' })
219 }
220 return res
221 }
222 })
223 simplify.simplifyCore = simplifyCore
224 simplify.resolve = resolve
225
226 function removeParens (node) {
227 return node.transform(function (node, path, parent) {
228 return isParenthesisNode(node)
229 ? removeParens(node.content)
230 : node
231 })
232 }
233
234 // All constants that are allowed in rules
235 const SUPPORTED_CONSTANTS = {
236 true: true,
237 false: true,
238 e: true,
239 i: true,
240 Infinity: true,
241 LN2: true,
242 LN10: true,
243 LOG2E: true,
244 LOG10E: true,
245 NaN: true,
246 phi: true,
247 pi: true,
248 SQRT1_2: true,
249 SQRT2: true,
250 tau: true
251 // null: false,
252 // undefined: false,
253 // version: false,
254 }
255
256 // Array of strings, used to build the ruleSet.
257 // Each l (left side) and r (right side) are parsed by
258 // the expression parser into a node tree.
259 // Left hand sides are matched to subtrees within the
260 // expression to be parsed and replaced with the right
261 // hand side.
262 // TODO: Add support for constraints on constants (either in the form of a '=' expression or a callback [callback allows things like comparing symbols alphabetically])
263 // To evaluate lhs constants for rhs constants, use: { l: 'c1+c2', r: 'c3', evaluate: 'c3 = c1 + c2' }. Multiple assignments are separated by ';' in block format.
264 // It is possible to get into an infinite loop with conflicting rules
265 simplify.rules = [
266 simplifyCore,
267 // { l: 'n+0', r: 'n' }, // simplifyCore
268 // { l: 'n^0', r: '1' }, // simplifyCore
269 // { l: '0*n', r: '0' }, // simplifyCore
270 // { l: 'n/n', r: '1'}, // simplifyCore
271 // { l: 'n^1', r: 'n' }, // simplifyCore
272 // { l: '+n1', r:'n1' }, // simplifyCore
273 // { l: 'n--n1', r:'n+n1' }, // simplifyCore
274 { l: 'log(e)', r: '1' },
275
276 // temporary rules
277 { l: 'n-n1', r: 'n+-n1' }, // temporarily replace 'subtract' so we can further flatten the 'add' operator
278 { l: '-(c*v)', r: '(-c) * v' }, // make non-constant terms positive
279 { l: '-v', r: '(-1) * v' },
280 { l: 'n/n1^n2', r: 'n*n1^-n2' }, // temporarily replace 'divide' so we can further flatten the 'multiply' operator
281 { l: 'n/n1', r: 'n*n1^-1' },
282
283 // expand nested exponentiation
284 { l: '(n ^ n1) ^ n2', r: 'n ^ (n1 * n2)' },
285
286 // collect like factors
287 { l: 'n*n', r: 'n^2' },
288 { l: 'n * n^n1', r: 'n^(n1+1)' },
289 { l: 'n^n1 * n^n2', r: 'n^(n1+n2)' },
290
291 // collect like terms
292 { l: 'n+n', r: '2*n' },
293 { l: 'n+-n', r: '0' },
294 { l: 'n1*n2 + n2', r: '(n1+1)*n2' },
295 { l: 'n1*n3 + n2*n3', r: '(n1+n2)*n3' },
296
297 // remove parenthesis in the case of negating a quantitiy
298 { l: 'n1 + -1 * (n2 + n3)', r: 'n1 + -1 * n2 + -1 * n3' },
299
300 simplifyConstant,
301
302 { l: '(-n)*n1', r: '-(n*n1)' }, // make factors positive (and undo 'make non-constant terms positive')
303
304 // ordering of constants
305 { l: 'c+v', r: 'v+c', context: { add: { commutative: false } } },
306 { l: 'v*c', r: 'c*v', context: { multiply: { commutative: false } } },
307
308 // undo temporary rules
309 // { l: '(-1) * n', r: '-n' }, // #811 added test which proved this is redundant
310 { l: 'n+-n1', r: 'n-n1' }, // undo replace 'subtract'
311 { l: 'n*(n1^-1)', r: 'n/n1' }, // undo replace 'divide'
312 { l: 'n*n1^-n2', r: 'n/n1^n2' },
313 { l: 'n1^-1', r: '1/n1' },
314
315 { l: 'n*(n1/n2)', r: '(n*n1)/n2' }, // '*' before '/'
316 { l: 'n-(n1+n2)', r: 'n-n1-n2' }, // '-' before '+'
317 // { l: '(n1/n2)/n3', r: 'n1/(n2*n3)' },
318 // { l: '(n*n1)/(n*n2)', r: 'n1/n2' },
319
320 { l: '1*n', r: 'n' } // this pattern can be produced by simplifyConstant
321
322 ]
323
324 /**
325 * Parse the string array of rules into nodes
326 *
327 * Example syntax for rules:
328 *
329 * Position constants to the left in a product:
330 * { l: 'n1 * c1', r: 'c1 * n1' }
331 * n1 is any Node, and c1 is a ConstantNode.
332 *
333 * Apply difference of squares formula:
334 * { l: '(n1 - n2) * (n1 + n2)', r: 'n1^2 - n2^2' }
335 * n1, n2 mean any Node.
336 *
337 * Short hand notation:
338 * 'n1 * c1 -> c1 * n1'
339 */
340 function _buildRules (rules) {
341 // Array of rules to be used to simplify expressions
342 const ruleSet = []
343 for (let i = 0; i < rules.length; i++) {
344 let rule = rules[i]
345 let newRule
346 const ruleType = typeof rule
347 switch (ruleType) {
348 case 'string':
349 {
350 const lr = rule.split('->')
351 if (lr.length === 2) {
352 rule = { l: lr[0], r: lr[1] }
353 } else {
354 throw SyntaxError('Could not parse rule: ' + rule)
355 }
356 }
357 /* falls through */
358 case 'object':
359 newRule = {
360 l: removeParens(parse(rule.l)),
361 r: removeParens(parse(rule.r))
362 }
363 if (rule.context) {
364 newRule.evaluate = rule.context
365 }
366 if (rule.evaluate) {
367 newRule.evaluate = parse(rule.evaluate)
368 }
369
370 if (isAssociative(newRule.l)) {
371 const makeNode = createMakeNodeFunction(newRule.l)
372 const expandsym = _getExpandPlaceholderSymbol()
373 newRule.expanded = {}
374 newRule.expanded.l = makeNode([newRule.l.clone(), expandsym])
375 // Push the expandsym into the deepest possible branch.
376 // This helps to match the newRule against nodes returned from getSplits() later on.
377 flatten(newRule.expanded.l)
378 unflattenr(newRule.expanded.l)
379 newRule.expanded.r = makeNode([newRule.r, expandsym])
380 }
381 break
382 case 'function':
383 newRule = rule
384 break
385 default:
386 throw TypeError('Unsupported type of rule: ' + ruleType)
387 }
388 // console.log('Adding rule: ' + rules[i])
389 // console.log(newRule)
390 ruleSet.push(newRule)
391 }
392 return ruleSet
393 }
394
395 let _lastsym = 0
396 function _getExpandPlaceholderSymbol () {
397 return new SymbolNode('_p' + _lastsym++)
398 }
399
400 /**
401 * Returns a simplfied form of node, or the original node if no simplification was possible.
402 *
403 * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} node
404 * @return {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} The simplified form of `expr`, or the original node if no simplification was possible.
405 */
406 const applyRule = typed('applyRule', {
407 'Node, Object': function (node, rule) {
408 // console.log('Entering applyRule(' + node.toString() + ')')
409
410 // Do not clone node unless we find a match
411 let res = node
412
413 // First replace our child nodes with their simplified versions
414 // If a child could not be simplified, the assignments will have
415 // no effect since the node is returned unchanged
416 if (res instanceof OperatorNode || res instanceof FunctionNode) {
417 if (res.args) {
418 for (let i = 0; i < res.args.length; i++) {
419 res.args[i] = applyRule(res.args[i], rule)
420 }
421 }
422 } else if (res instanceof ParenthesisNode) {
423 if (res.content) {
424 res.content = applyRule(res.content, rule)
425 }
426 }
427
428 // Try to match a rule against this node
429 let repl = rule.r
430 let matches = _ruleMatch(rule.l, res)[0]
431
432 // If the rule is associative operator, we can try matching it while allowing additional terms.
433 // This allows us to match rules like 'n+n' to the expression '(1+x)+x' or even 'x+1+x' if the operator is commutative.
434 if (!matches && rule.expanded) {
435 repl = rule.expanded.r
436 matches = _ruleMatch(rule.expanded.l, res)[0]
437 }
438
439 if (matches) {
440 // const before = res.toString({parenthesis: 'all'})
441
442 // Create a new node by cloning the rhs of the matched rule
443 // we keep any implicit multiplication state if relevant
444 const implicit = res.implicit
445 res = repl.clone()
446 if (implicit && 'implicit' in repl) {
447 res.implicit = true
448 }
449
450 // Replace placeholders with their respective nodes without traversing deeper into the replaced nodes
451 res = res.transform(function (node) {
452 if (node.isSymbolNode && hasOwnProperty(matches.placeholders, node.name)) {
453 return matches.placeholders[node.name].clone()
454 } else {
455 return node
456 }
457 })
458
459 // const after = res.toString({parenthesis: 'all'})
460 // console.log('Simplified ' + before + ' to ' + after)
461 }
462
463 return res
464 }
465 })
466
467 /**
468 * Get (binary) combinations of a flattened binary node
469 * e.g. +(node1, node2, node3) -> [
470 * +(node1, +(node2, node3)),
471 * +(node2, +(node1, node3)),
472 * +(node3, +(node1, node2))]
473 *
474 */
475 function getSplits (node, context) {
476 const res = []
477 let right, rightArgs
478 const makeNode = createMakeNodeFunction(node)
479 if (isCommutative(node, context)) {
480 for (let i = 0; i < node.args.length; i++) {
481 rightArgs = node.args.slice(0)
482 rightArgs.splice(i, 1)
483 right = (rightArgs.length === 1) ? rightArgs[0] : makeNode(rightArgs)
484 res.push(makeNode([node.args[i], right]))
485 }
486 } else {
487 rightArgs = node.args.slice(1)
488 right = (rightArgs.length === 1) ? rightArgs[0] : makeNode(rightArgs)
489 res.push(makeNode([node.args[0], right]))
490 }
491 return res
492 }
493
494 /**
495 * Returns the set union of two match-placeholders or null if there is a conflict.
496 */
497 function mergeMatch (match1, match2) {
498 const res = { placeholders: {} }
499
500 // Some matches may not have placeholders; this is OK
501 if (!match1.placeholders && !match2.placeholders) {
502 return res
503 } else if (!match1.placeholders) {
504 return match2
505 } else if (!match2.placeholders) {
506 return match1
507 }
508
509 // Placeholders with the same key must match exactly
510 for (const key in match1.placeholders) {
511 res.placeholders[key] = match1.placeholders[key]
512 if (hasOwnProperty(match2.placeholders, key)) {
513 if (!_exactMatch(match1.placeholders[key], match2.placeholders[key])) {
514 return null
515 }
516 }
517 }
518
519 for (const key in match2.placeholders) {
520 res.placeholders[key] = match2.placeholders[key]
521 }
522
523 return res
524 }
525
526 /**
527 * Combine two lists of matches by applying mergeMatch to the cartesian product of two lists of matches.
528 * Each list represents matches found in one child of a node.
529 */
530 function combineChildMatches (list1, list2) {
531 const res = []
532
533 if (list1.length === 0 || list2.length === 0) {
534 return res
535 }
536
537 let merged
538 for (let i1 = 0; i1 < list1.length; i1++) {
539 for (let i2 = 0; i2 < list2.length; i2++) {
540 merged = mergeMatch(list1[i1], list2[i2])
541 if (merged) {
542 res.push(merged)
543 }
544 }
545 }
546 return res
547 }
548
549 /**
550 * Combine multiple lists of matches by applying mergeMatch to the cartesian product of two lists of matches.
551 * Each list represents matches found in one child of a node.
552 * Returns a list of unique matches.
553 */
554 function mergeChildMatches (childMatches) {
555 if (childMatches.length === 0) {
556 return childMatches
557 }
558
559 const sets = childMatches.reduce(combineChildMatches)
560 const uniqueSets = []
561 const unique = {}
562 for (let i = 0; i < sets.length; i++) {
563 const s = JSON.stringify(sets[i])
564 if (!unique[s]) {
565 unique[s] = true
566 uniqueSets.push(sets[i])
567 }
568 }
569 return uniqueSets
570 }
571
572 /**
573 * Determines whether node matches rule.
574 *
575 * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} rule
576 * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} node
577 * @return {Object} Information about the match, if it exists.
578 */
579 function _ruleMatch (rule, node, isSplit) {
580 // console.log('Entering _ruleMatch(' + JSON.stringify(rule) + ', ' + JSON.stringify(node) + ')')
581 // console.log('rule = ' + rule)
582 // console.log('node = ' + node)
583
584 // console.log('Entering _ruleMatch(' + rule.toString() + ', ' + node.toString() + ')')
585 let res = [{ placeholders: {} }]
586
587 if ((rule instanceof OperatorNode && node instanceof OperatorNode) ||
588 (rule instanceof FunctionNode && node instanceof FunctionNode)) {
589 // If the rule is an OperatorNode or a FunctionNode, then node must match exactly
590 if (rule instanceof OperatorNode) {
591 if (rule.op !== node.op || rule.fn !== node.fn) {
592 return []
593 }
594 } else if (rule instanceof FunctionNode) {
595 if (rule.name !== node.name) {
596 return []
597 }
598 }
599
600 // rule and node match. Search the children of rule and node.
601 if ((node.args.length === 1 && rule.args.length === 1) || !isAssociative(node) || isSplit) {
602 // Expect non-associative operators to match exactly
603 const childMatches = []
604 for (let i = 0; i < rule.args.length; i++) {
605 const childMatch = _ruleMatch(rule.args[i], node.args[i])
606 if (childMatch.length === 0) {
607 // Child did not match, so stop searching immediately
608 return []
609 }
610 // The child matched, so add the information returned from the child to our result
611 childMatches.push(childMatch)
612 }
613 res = mergeChildMatches(childMatches)
614 } else if (node.args.length >= 2 && rule.args.length === 2) { // node is flattened, rule is not
615 // Associative operators/functions can be split in different ways so we check if the rule matches each
616 // them and return their union.
617 const splits = getSplits(node, rule.context)
618 let splitMatches = []
619 for (let i = 0; i < splits.length; i++) {
620 const matchSet = _ruleMatch(rule, splits[i], true) // recursing at the same tree depth here
621 splitMatches = splitMatches.concat(matchSet)
622 }
623 return splitMatches
624 } else if (rule.args.length > 2) {
625 throw Error('Unexpected non-binary associative function: ' + rule.toString())
626 } else {
627 // Incorrect number of arguments in rule and node, so no match
628 return []
629 }
630 } else if (rule instanceof SymbolNode) {
631 // If the rule is a SymbolNode, then it carries a special meaning
632 // according to the first character of the symbol node name.
633 // c.* matches a ConstantNode
634 // n.* matches any node
635 if (rule.name.length === 0) {
636 throw new Error('Symbol in rule has 0 length...!?')
637 }
638 if (SUPPORTED_CONSTANTS[rule.name]) {
639 // built-in constant must match exactly
640 if (rule.name !== node.name) {
641 return []
642 }
643 } else if (rule.name[0] === 'n' || rule.name.substring(0, 2) === '_p') {
644 // rule matches _anything_, so assign this node to the rule.name placeholder
645 // Assign node to the rule.name placeholder.
646 // Our parent will check for matches among placeholders.
647 res[0].placeholders[rule.name] = node
648 } else if (rule.name[0] === 'v') {
649 // rule matches any variable thing (not a ConstantNode)
650 if (!isConstantNode(node)) {
651 res[0].placeholders[rule.name] = node
652 } else {
653 // Mis-match: rule was expecting something other than a ConstantNode
654 return []
655 }
656 } else if (rule.name[0] === 'c') {
657 // rule matches any ConstantNode
658 if (node instanceof ConstantNode) {
659 res[0].placeholders[rule.name] = node
660 } else {
661 // Mis-match: rule was expecting a ConstantNode
662 return []
663 }
664 } else {
665 throw new Error('Invalid symbol in rule: ' + rule.name)
666 }
667 } else if (rule instanceof ConstantNode) {
668 // Literal constant must match exactly
669 if (!equal(rule.value, node.value)) {
670 return []
671 }
672 } else {
673 // Some other node was encountered which we aren't prepared for, so no match
674 return []
675 }
676
677 // It's a match!
678
679 // console.log('_ruleMatch(' + rule.toString() + ', ' + node.toString() + ') found a match')
680 return res
681 }
682
683 /**
684 * Determines whether p and q (and all their children nodes) are identical.
685 *
686 * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} p
687 * @param {ConstantNode | SymbolNode | ParenthesisNode | FunctionNode | OperatorNode} q
688 * @return {Object} Information about the match, if it exists.
689 */
690 function _exactMatch (p, q) {
691 if (p instanceof ConstantNode && q instanceof ConstantNode) {
692 if (!equal(p.value, q.value)) {
693 return false
694 }
695 } else if (p instanceof SymbolNode && q instanceof SymbolNode) {
696 if (p.name !== q.name) {
697 return false
698 }
699 } else if ((p instanceof OperatorNode && q instanceof OperatorNode) ||
700 (p instanceof FunctionNode && q instanceof FunctionNode)) {
701 if (p instanceof OperatorNode) {
702 if (p.op !== q.op || p.fn !== q.fn) {
703 return false
704 }
705 } else if (p instanceof FunctionNode) {
706 if (p.name !== q.name) {
707 return false
708 }
709 }
710
711 if (p.args.length !== q.args.length) {
712 return false
713 }
714
715 for (let i = 0; i < p.args.length; i++) {
716 if (!_exactMatch(p.args[i], q.args[i])) {
717 return false
718 }
719 }
720 } else {
721 return false
722 }
723
724 return true
725 }
726
727 return simplify
728})