Skip to content

Commit 46c5946

Browse files
committed
refactor(resolve): Split implementation into distinct Node types
This refactor allows custom resolve methods for different node types or for newly added node types, as an alternative to josdejong#2775. Adds a test that demonstrates the creationg of a (stub) IntervalNode, as if you were doing interval arithmetic, say, with a custom resolve method for the new type.
1 parent f1f8326 commit 46c5946

File tree

3 files changed

+132
-51
lines changed

3 files changed

+132
-51
lines changed

src/core/function/typed.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,15 +152,16 @@ export const createTyped = /* #__PURE__ */ factory('typed', dependencies, functi
152152
{ name: 'FunctionNode', test: isFunctionNode },
153153
{ name: 'FunctionAssignmentNode', test: isFunctionAssignmentNode },
154154
{ name: 'IndexNode', test: isIndexNode },
155-
{ name: 'Node', test: isNode },
156155
{ name: 'ObjectNode', test: isObjectNode },
157156
{ name: 'OperatorNode', test: isOperatorNode },
158157
{ name: 'ParenthesisNode', test: isParenthesisNode },
159158
{ name: 'RangeNode', test: isRangeNode },
160159
{ name: 'RelationalNode', test: isRelationalNode },
161160
{ name: 'SymbolNode', test: isSymbolNode },
161+
{ name: 'Node', test: isNode },
162162

163163
{ name: 'Map', test: isMap },
164+
{ name: 'Set', test: entity => entity instanceof Set },
164165
{ name: 'Object', test: isObject } // order 'Object' last, it matches on other classes too
165166
])
166167

src/function/algebra/resolve.js

Lines changed: 79 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createMap } from '../../utils/map.js'
2-
import { isFunctionNode, isNode, isOperatorNode, isParenthesisNode, isSymbolNode } from '../../utils/is.js'
2+
import { isNode } from '../../utils/is.js'
33
import { factory } from '../../utils/factory.js'
44

55
const name = 'resolve'
@@ -46,65 +46,94 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({
4646
* If there is a cyclic dependency among the variables in `scope`,
4747
* resolution is impossible and a ReferenceError is thrown.
4848
*/
49-
function _resolve (node, scope, within = new Set()) { // note `within`:
50-
// `within` is not documented, since it is for internal cycle
51-
// detection only
52-
if (!scope) {
53-
return node
54-
}
55-
if (isSymbolNode(node)) {
56-
if (within.has(node.name)) {
57-
const variables = Array.from(within).join(', ')
58-
throw new ReferenceError(
59-
`recursive loop of variable definitions among {${variables}}`
60-
)
49+
50+
// First we set up core implementations for different node types
51+
// Note the 'within' argument that they all take is not documented, as it is
52+
// used only for internal cycle detection
53+
const resolvers = {
54+
SymbolNode: typed.referToSelf(self =>
55+
(symbol, scope, within = new Set()) => {
56+
// The key case for resolve; most other nodes we just recurse.
57+
if (!scope) return symbol
58+
if (within.has(symbol.name)) {
59+
const variables = Array.from(within).join(', ')
60+
throw new ReferenceError(
61+
`recursive loop of variable definitions among {${variables}}`
62+
)
63+
}
64+
const value = scope.get(symbol.name)
65+
if (isNode(value)) {
66+
const nextWithin = new Set(within)
67+
nextWithin.add(symbol.name)
68+
return self(value, scope, nextWithin)
69+
}
70+
if (typeof value === 'number') {
71+
return parse(String(value)) // ?? is this just to get the currently
72+
// defined behavior for number literals, i.e. maybe numbers are
73+
// currently being coerced to BigNumber?
74+
}
75+
if (value !== undefined) {
76+
return new ConstantNode(value)
77+
}
78+
return symbol
79+
}
80+
),
81+
OperatorNode: typed.referToSelf(self =>
82+
(operator, scope, within = new Set()) => {
83+
const args = operator.args.map(arg => self(arg, scope, within))
84+
// Has its own implementation because we don't recurse on the op also
85+
return new OperatorNode(
86+
operator.op, operator.fn, args, operator.implicit)
6187
}
62-
const value = scope.get(node.name)
63-
if (isNode(value)) {
64-
const nextWithin = new Set(within)
65-
nextWithin.add(node.name)
66-
return _resolve(value, scope, nextWithin)
67-
} else if (typeof value === 'number') {
68-
return parse(String(value))
69-
} else if (value !== undefined) {
70-
return new ConstantNode(value)
71-
} else {
72-
return node
88+
),
89+
FunctionNode: typed.referToSelf(self =>
90+
(func, scope, within = new Set()) => {
91+
const args = func.args.map(arg => self(arg, scope, within))
92+
// The only reason this has a separate implementation of its own
93+
// is that we don't resolve the func.name itself. But is that
94+
// really right? If the tree being resolved was the parse of
95+
// 'f(x,y)' and 'f' is defined in the scope, is it clear that we
96+
// don't want to replace the function symbol, too? Anyhow, leaving
97+
// the implementation as it was before the refactoring.
98+
return new FunctionNode(func.name, args)
7399
}
74-
} else if (isOperatorNode(node)) {
75-
const args = node.args.map(function (arg) {
76-
return _resolve(arg, scope, within)
77-
})
78-
return new OperatorNode(node.op, node.fn, args, node.implicit)
79-
} else if (isParenthesisNode(node)) {
80-
return new ParenthesisNode(_resolve(node.content, scope, within))
81-
} else if (isFunctionNode(node)) {
82-
const args = node.args.map(function (arg) {
83-
return _resolve(arg, scope, within)
84-
})
85-
return new FunctionNode(node.name, args)
86-
}
100+
),
101+
Node: typed.referToSelf(self =>
102+
(node, scope, within = new Set()) => {
103+
// The generic case: just recurse
104+
return node.map(child => self(child, scope, within))
105+
}
106+
)
107+
}
87108

88-
// Otherwise just recursively resolve any children (might also work
89-
// for some of the above special cases)
90-
return node.map(child => _resolve(child, scope, within))
109+
// Now expand with all the possible argument types:
110+
const nodeTypes = Object.keys(resolvers)
111+
const scopeType = ', Map | null | undefined'
112+
const objType = ', Object'
113+
const withinType = ', Set'
114+
for (const nodeType of nodeTypes) {
115+
resolvers[nodeType + scopeType] = resolvers[nodeType]
116+
resolvers[nodeType + scopeType + withinType] = resolvers[nodeType]
117+
resolvers[nodeType + objType] = typed.referToSelf(self =>
118+
(node, objScope) => self(node, createMap(objScope))
119+
)
120+
// Don't need to do nodeType + objType + withinType since we only get
121+
// an obj instead of a Map scope in the outermost call, which has no
122+
// "within" argument.
91123
}
92124

93-
return typed('resolve', {
94-
Node: _resolve,
95-
'Node, Map | null | undefined': _resolve,
96-
'Node, Object': (n, scope) => _resolve(n, createMap(scope)),
97-
// For arrays and matrices, we map `self` rather than `_resolve`
98-
// because resolve is fairly expensive anyway, and this way
99-
// we get nice error messages if one entry in the array has wrong type.
125+
// Now add the array and matrix types:
126+
Object.assign(resolvers, {
100127
'Array | Matrix': typed.referToSelf(self => A => A.map(n => self(n))),
101128
'Array | Matrix, null | undefined': typed.referToSelf(
102129
self => A => A.map(n => self(n))),
130+
'Array | Matrix, Map': typed.referToSelf(
131+
self => (A, scope) => A.map(n => self(n, scope))),
103132
'Array, Object': typed.referTo(
104133
'Array,Map', selfAM => (A, scope) => selfAM(A, createMap(scope))),
105134
'Matrix, Object': typed.referTo(
106-
'Matrix,Map', selfMM => (A, scope) => selfMM(A, createMap(scope))),
107-
'Array | Matrix, Map': typed.referToSelf(
108-
self => (A, scope) => A.map(n => self(n, scope)))
135+
'Matrix,Map', selfMM => (A, scope) => selfMM(A, createMap(scope)))
109136
})
137+
138+
return typed('resolve', resolvers)
110139
})

test/unit-tests/function/algebra/resolve.test.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,55 @@ describe('resolve', function () {
102102
}),
103103
/ReferenceError.*\{x, y, z\}/)
104104
})
105+
106+
it('should allow resolving custom nodes in custom ways', function () {
107+
const mymath = math.create()
108+
const Node = mymath.Node
109+
class IntervalNode extends Node {
110+
// a node that represents any value in a closed interval
111+
constructor (left, right) {
112+
super()
113+
this.left = left
114+
this.right = right
115+
}
116+
117+
static name = 'IntervalNode'
118+
get type () { return 'IntervalNode' }
119+
get isIntervalNode () { return true }
120+
clone () {
121+
return new IntervalNode(this.left, this.right)
122+
}
123+
124+
_toString (options) {
125+
return `[|${this.left}, ${this.right}|]`
126+
}
127+
128+
midpoint () {
129+
return (this.left + this.right) / 2
130+
}
131+
}
132+
133+
mymath.typed.addTypes(
134+
[{
135+
name: 'IntervalNode',
136+
test: entity => entity && entity.isIntervalNode
137+
}],
138+
'RangeNode') // Insert just before RangeNode in type order
139+
140+
// IntervalNodes resolve to their midpoint:
141+
const intervalResolver = node => new mymath.ConstantNode(node.midpoint())
142+
const resolveInterval = mymath.typed({
143+
IntervalNode: intervalResolver,
144+
'IntervalNode, Object|Map|null|undefined': intervalResolver,
145+
'IntervalNode, Map|null|undefined, Set': intervalResolver
146+
})
147+
// Merge with standard resolve:
148+
mymath.import({ resolve: resolveInterval })
149+
150+
// And finally test:
151+
const innerNode = new IntervalNode(1, 3)
152+
const outerNode = new mymath.OperatorNode(
153+
'+', 'add', [innerNode, new mymath.ConstantNode(4)])
154+
assert.strictEqual(mymath.resolve(outerNode).toString(), '2 + 4')
155+
})
105156
})

0 commit comments

Comments
 (0)