Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Proposal] Refactor resolve fn to be more flexible and extensible #2775

Draft
wants to merge 2 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/expression/node/FunctionNode.js
Expand Up @@ -278,6 +278,16 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
}
}

/**
* TODO
* @param {*} scope
* @param {*} within
* @returns
*/
resolve (scope, within = new Set()) {
return new FunctionNode(this.name, this.args.map((a) => a.resolve(scope, within)))
}

/**
* Execute a callback for each of the child nodes of this node
* @param {function(child: Node, path: string, parent: Node)} callback
Expand Down
7 changes: 7 additions & 0 deletions src/expression/node/Node.js
Expand Up @@ -59,6 +59,13 @@ export const createNode = /* #__PURE__ */ factory(name, dependencies, ({ mathWit
}
}

/**
* TODO
*/
resolve (scope, within = new Set()) {
return this.map(child => child.resolve(scope, within))
}

/**
* Compile a node into a JavaScript function.
* This basically pre-calculates as much as possible and only leaves open
Expand Down
10 changes: 10 additions & 0 deletions src/expression/node/OperatorNode.js
Expand Up @@ -326,6 +326,16 @@ export const createOperatorNode = /* #__PURE__ */ factory(name, dependencies, ({
}
}

/**
* TODO
* @param {*} scope
* @param {*} within
* @returns
*/
resolve (scope, within = new Set()) {
return new OperatorNode(this.op, this.fn, this.args.map(a => a.resolve(scope, within)), this.implicit)
}

/**
* Execute a callback for each of the child nodes of this node
* @param {function(child: Node, path: string, parent: Node)} callback
Expand Down
10 changes: 10 additions & 0 deletions src/expression/node/ParenthesisNode.js
Expand Up @@ -46,6 +46,16 @@ export const createParenthesisNode = /* #__PURE__ */ factory(name, dependencies,
return this.content._compile(math, argNames)
}

/**
* TODO
* @param {*} scope
* @param {*} within
* @returns
*/
resolve (scope, within = new Set()) {
return new ParenthesisNode(this.content.resolve(scope, within))
}

/**
* Get the content of the current Node.
* @return {Node} content
Expand Down
35 changes: 33 additions & 2 deletions src/expression/node/SymbolNode.js
@@ -1,16 +1,18 @@
import { escape } from '../../utils/string.js'
import { getSafeProperty } from '../../utils/customs.js'
import { isNode } from '../../utils/is.js'
import { factory } from '../../utils/factory.js'
import { toSymbol } from '../../utils/latex.js'

const name = 'SymbolNode'
const dependencies = [
'math',
'?Unit',
'Node'
'Node',
'ConstantNode'
]

export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ math, Unit, Node }) => {
export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ math, Unit, Node, ConstantNode, parse }) => {
/**
* Check whether some name is a valueless unit like "inch".
* @param {string} name
Expand Down Expand Up @@ -41,6 +43,35 @@ export const createSymbolNode = /* #__PURE__ */ factory(name, dependencies, ({ m
get type () { return 'SymbolNode' }
get isSymbolNode () { return true }

/**
* TODO
* @param {*} scope
* @param {*} within
* @returns
*/
resolve (scope, within = new Set()) {
if (within.has(this.name)) {
const variables = Array.from(within).join(', ')
throw new ReferenceError(
`recursive loop of variable definitions among {${variables}}`
)
}

const value = scope.get(this.name)

if (isNode(value)) {
const nextWithin = new Set(within)
nextWithin.add(this.name)
return value.resolve(scope, nextWithin)
} else if (typeof value === 'number') {
return math.parse(String(value))
} else if (value !== undefined) {
return new ConstantNode(value)
} else {
return this
}
}

/**
* Compile a node into a JavaScript function.
* This basically pre-calculates as much as possible and only leaves open
Expand Down
51 changes: 3 additions & 48 deletions src/function/algebra/resolve.js
@@ -1,24 +1,13 @@
import { createMap } from '../../utils/map.js'
import { isFunctionNode, isNode, isOperatorNode, isParenthesisNode, isSymbolNode } from '../../utils/is.js'
import { factory } from '../../utils/factory.js'

const name = 'resolve'
const dependencies = [
'typed',
'parse',
'ConstantNode',
'FunctionNode',
'OperatorNode',
'ParenthesisNode'
'typed'
]

export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({
typed,
parse,
ConstantNode,
FunctionNode,
OperatorNode,
ParenthesisNode
typed
}) => {
/**
* resolve(expr, scope) replaces variable nodes with their scoped values
Expand Down Expand Up @@ -52,42 +41,8 @@ export const createResolve = /* #__PURE__ */ factory(name, dependencies, ({
if (!scope) {
return node
}
if (isSymbolNode(node)) {
if (within.has(node.name)) {
const variables = Array.from(within).join(', ')
throw new ReferenceError(
`recursive loop of variable definitions among {${variables}}`
)
}
const value = scope.get(node.name)
if (isNode(value)) {
const nextWithin = new Set(within)
nextWithin.add(node.name)
return _resolve(value, scope, nextWithin)
} else if (typeof value === 'number') {
return parse(String(value))
} else if (value !== undefined) {
return new ConstantNode(value)
} else {
return node
}
} else if (isOperatorNode(node)) {
const args = node.args.map(function (arg) {
return _resolve(arg, scope, within)
})
return new OperatorNode(node.op, node.fn, args, node.implicit)
} else if (isParenthesisNode(node)) {
return new ParenthesisNode(_resolve(node.content, scope, within))
} else if (isFunctionNode(node)) {
const args = node.args.map(function (arg) {
return _resolve(arg, scope, within)
})
return new FunctionNode(node.name, args)
}

// Otherwise just recursively resolve any children (might also work
// for some of the above special cases)
return node.map(child => _resolve(child, scope, within))
return node.resolve(scope, within)
}

return typed('resolve', {
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/expression/node/OperatorNode.test.js
Expand Up @@ -45,6 +45,10 @@ describe('OperatorNode', function () {
assert.strictEqual(add23.compile().evaluate(), 5)
})

it('should resolve an OperatorNode', function () {
// TODO
})

it('should test whether a unary or binary operator', function () {
const n1 = new OperatorNode('-', 'unaryMinus', [two])
assert.strictEqual(n1.isUnary(), true)
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/expression/node/ParenthesisNode.test.js
Expand Up @@ -33,6 +33,10 @@ describe('ParenthesisNode', function () {
assert.strictEqual(n.compile().evaluate.toString(), a.compile().evaluate.toString())
})

it('should resolve a ParenthesisNode', function () {
// TODO
})

it('should filter a ParenthesisNode', function () {
const a = new ConstantNode(1)
const n = new ParenthesisNode(a)
Expand Down
4 changes: 4 additions & 0 deletions test/unit-tests/expression/node/SymbolNode.test.js
Expand Up @@ -50,6 +50,10 @@ describe('SymbolNode', function () {
assert.strictEqual(expr2.evaluate(scope2), math.sqrt)
})

it('should resolve a SymbolNode', function () {
// TODO
})

it('should filter a SymbolNode', function () {
const n = new SymbolNode('x')
assert.deepStrictEqual(n.filter(function (node) { return node instanceof SymbolNode }), [n])
Expand Down