mirror of
https://github.com/josdejong/mathjs.git
synced 2026-01-18 14:59:29 +00:00
1708 lines
44 KiB
JavaScript
1708 lines
44 KiB
JavaScript
'use strict'
|
|
|
|
import { factory } from '../utils/factory'
|
|
import { isAccessorNode, isConstantNode, isFunctionNode, isOperatorNode, isSymbolNode } from '../utils/is'
|
|
import { deepMap } from '../utils/collection'
|
|
|
|
const name = 'parse'
|
|
const dependencies = [
|
|
'typed',
|
|
'numeric',
|
|
'config',
|
|
'AccessorNode',
|
|
'ArrayNode',
|
|
'AssignmentNode',
|
|
'BlockNode',
|
|
'ConditionalNode',
|
|
'ConstantNode',
|
|
'FunctionAssignmentNode',
|
|
'FunctionNode',
|
|
'IndexNode',
|
|
'ObjectNode',
|
|
'OperatorNode',
|
|
'ParenthesisNode',
|
|
'RangeNode',
|
|
'RelationalNode',
|
|
'SymbolNode'
|
|
]
|
|
|
|
export const createParse = /* #__PURE__ */ factory(name, dependencies, ({
|
|
typed,
|
|
numeric,
|
|
config,
|
|
AccessorNode,
|
|
ArrayNode,
|
|
AssignmentNode,
|
|
BlockNode,
|
|
ConditionalNode,
|
|
ConstantNode,
|
|
FunctionAssignmentNode,
|
|
FunctionNode,
|
|
IndexNode,
|
|
ObjectNode,
|
|
OperatorNode,
|
|
ParenthesisNode,
|
|
RangeNode,
|
|
RelationalNode,
|
|
SymbolNode
|
|
}) => {
|
|
/**
|
|
* Parse an expression. Returns a node tree, which can be evaluated by
|
|
* invoking node.evaluate().
|
|
*
|
|
* Note the evaluating arbitrary expressions may involve security risks,
|
|
* see [https://mathjs.org/docs/expressions/security.html](https://mathjs.org/docs/expressions/security.html) for more information.
|
|
*
|
|
* Syntax:
|
|
*
|
|
* math.parse(expr)
|
|
* math.parse(expr, options)
|
|
* math.parse([expr1, expr2, expr3, ...])
|
|
* math.parse([expr1, expr2, expr3, ...], options)
|
|
*
|
|
* Example:
|
|
*
|
|
* const node1 = math.parse('sqrt(3^2 + 4^2)')
|
|
* node1.compile().evaluate() // 5
|
|
*
|
|
* let scope = {a:3, b:4}
|
|
* const node2 = math.parse('a * b') // 12
|
|
* const code2 = node2.compile()
|
|
* code2.evaluate(scope) // 12
|
|
* scope.a = 5
|
|
* code2.evaluate(scope) // 20
|
|
*
|
|
* const nodes = math.parse(['a = 3', 'b = 4', 'a * b'])
|
|
* nodes[2].compile().evaluate() // 12
|
|
*
|
|
* See also:
|
|
*
|
|
* evaluate, compile
|
|
*
|
|
* @param {string | string[] | Matrix} expr Expression to be parsed
|
|
* @param {{nodes: Object<string, Node>}} [options] Available options:
|
|
* - `nodes` a set of custom nodes
|
|
* @return {Node | Node[]} node
|
|
* @throws {Error}
|
|
*/
|
|
const parse = typed(name, {
|
|
'string': function (expression) {
|
|
return parseStart(expression, {})
|
|
},
|
|
'Array | Matrix': function (expressions) {
|
|
return parseMultiple(expressions, {})
|
|
},
|
|
'string, Object': function (expression, options) {
|
|
const extraNodes = options.nodes !== undefined ? options.nodes : {}
|
|
|
|
return parseStart(expression, extraNodes)
|
|
},
|
|
'Array | Matrix, Object': parseMultiple
|
|
})
|
|
|
|
function parseMultiple (expressions, options = {}) {
|
|
const extraNodes = options.nodes !== undefined ? options.nodes : {}
|
|
|
|
// parse an array or matrix with expressions
|
|
return deepMap(expressions, function (elem) {
|
|
if (typeof elem !== 'string') throw new TypeError('String expected')
|
|
|
|
return parseStart(elem, extraNodes)
|
|
})
|
|
}
|
|
|
|
// token types enumeration
|
|
const TOKENTYPE = {
|
|
NULL: 0,
|
|
DELIMITER: 1,
|
|
NUMBER: 2,
|
|
SYMBOL: 3,
|
|
UNKNOWN: 4
|
|
}
|
|
|
|
// map with all delimiters
|
|
const DELIMITERS = {
|
|
',': true,
|
|
'(': true,
|
|
')': true,
|
|
'[': true,
|
|
']': true,
|
|
'{': true,
|
|
'}': true,
|
|
'"': true,
|
|
'\'': true,
|
|
';': true,
|
|
|
|
'+': true,
|
|
'-': true,
|
|
'*': true,
|
|
'.*': true,
|
|
'/': true,
|
|
'./': true,
|
|
'%': true,
|
|
'^': true,
|
|
'.^': true,
|
|
'~': true,
|
|
'!': true,
|
|
'&': true,
|
|
'|': true,
|
|
'^|': true,
|
|
'=': true,
|
|
':': true,
|
|
'?': true,
|
|
|
|
'==': true,
|
|
'!=': true,
|
|
'<': true,
|
|
'>': true,
|
|
'<=': true,
|
|
'>=': true,
|
|
|
|
'<<': true,
|
|
'>>': true,
|
|
'>>>': true
|
|
}
|
|
|
|
// map with all named delimiters
|
|
const NAMED_DELIMITERS = {
|
|
'mod': true,
|
|
'to': true,
|
|
'in': true,
|
|
'and': true,
|
|
'xor': true,
|
|
'or': true,
|
|
'not': true
|
|
}
|
|
|
|
const CONSTANTS = {
|
|
'true': true,
|
|
'false': false,
|
|
'null': null,
|
|
'undefined': undefined
|
|
}
|
|
|
|
const NUMERIC_CONSTANTS = [
|
|
'NaN',
|
|
'Infinity'
|
|
]
|
|
|
|
function initialState () {
|
|
return {
|
|
extraNodes: {}, // current extra nodes, must be careful not to mutate
|
|
expression: '', // current expression
|
|
comment: '', // last parsed comment
|
|
index: 0, // current index in expr
|
|
token: '', // current token
|
|
tokenType: TOKENTYPE.NULL, // type of the token
|
|
nestingLevel: 0, // level of nesting inside parameters, used to ignore newline characters
|
|
conditionalLevel: null // when a conditional is being parsed, the level of the conditional is stored here
|
|
}
|
|
}
|
|
|
|
/**
|
|
* View upto `length` characters of the expression starting at the current character.
|
|
*
|
|
* @param {State} state
|
|
* @param {number} [length=1] Number of characters to view
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
function currentString (state, length) {
|
|
return state.expression.substr(state.index, length)
|
|
}
|
|
|
|
/**
|
|
* View the current character. Returns '' if end of expression is reached.
|
|
*
|
|
* @param {State} state
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
function currentCharacter (state) {
|
|
return currentString(state, 1)
|
|
}
|
|
|
|
/**
|
|
* Get the next character from the expression.
|
|
* The character is stored into the char c. If the end of the expression is
|
|
* reached, the function puts an empty string in c.
|
|
* @private
|
|
*/
|
|
function next (state) {
|
|
state.index++
|
|
}
|
|
|
|
/**
|
|
* Preview the previous character from the expression.
|
|
* @return {string} cNext
|
|
* @private
|
|
*/
|
|
function prevCharacter (state) {
|
|
return state.expression.charAt(state.index - 1)
|
|
}
|
|
|
|
/**
|
|
* Preview the next character from the expression.
|
|
* @return {string} cNext
|
|
* @private
|
|
*/
|
|
function nextCharacter (state) {
|
|
return state.expression.charAt(state.index + 1)
|
|
}
|
|
|
|
/**
|
|
* Get next token in the current string expr.
|
|
* The token and token type are available as token and tokenType
|
|
* @private
|
|
*/
|
|
function getToken (state) {
|
|
state.tokenType = TOKENTYPE.NULL
|
|
state.token = ''
|
|
state.comment = ''
|
|
|
|
// skip over whitespaces
|
|
// space, tab, and newline when inside parameters
|
|
while (parse.isWhitespace(currentCharacter(state), state.nestingLevel)) {
|
|
next(state)
|
|
}
|
|
|
|
// skip comment
|
|
if (currentCharacter(state) === '#') {
|
|
while (currentCharacter(state) !== '\n' && currentCharacter(state) !== '') {
|
|
state.comment += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
}
|
|
|
|
// check for end of expression
|
|
if (currentCharacter(state) === '') {
|
|
// token is still empty
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
return
|
|
}
|
|
|
|
// check for new line character
|
|
if (currentCharacter(state) === '\n' && !state.nestingLevel) {
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
state.token = currentCharacter(state)
|
|
next(state)
|
|
return
|
|
}
|
|
|
|
const c1 = currentCharacter(state)
|
|
const c2 = currentString(state, 2)
|
|
const c3 = currentString(state, 3)
|
|
if (c3.length === 3 && DELIMITERS[c3]) {
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
state.token = c3
|
|
next(state)
|
|
next(state)
|
|
next(state)
|
|
return
|
|
}
|
|
|
|
// check for delimiters consisting of 2 characters
|
|
if (c2.length === 2 && DELIMITERS[c2]) {
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
state.token = c2
|
|
next(state)
|
|
next(state)
|
|
return
|
|
}
|
|
|
|
// check for delimiters consisting of 1 character
|
|
if (DELIMITERS[c1]) {
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
state.token = c1
|
|
next(state)
|
|
return
|
|
}
|
|
|
|
// check for a number
|
|
if (parse.isDigitDot(c1)) {
|
|
state.tokenType = TOKENTYPE.NUMBER
|
|
|
|
// get number, can have a single dot
|
|
if (currentCharacter(state) === '.') {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
|
|
if (!parse.isDigit(currentCharacter(state))) {
|
|
// this is no number, it is just a dot (can be dot notation)
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
}
|
|
} else {
|
|
while (parse.isDigit(currentCharacter(state))) {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
if (parse.isDecimalMark(currentCharacter(state), nextCharacter(state))) {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
}
|
|
|
|
while (parse.isDigit(currentCharacter(state))) {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
// check for exponential notation like "2.3e-4", "1.23e50" or "2e+4"
|
|
if (currentCharacter(state) === 'E' || currentCharacter(state) === 'e') {
|
|
if (parse.isDigit(nextCharacter(state)) || nextCharacter(state) === '-' || nextCharacter(state) === '+') {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
|
|
if (currentCharacter(state) === '+' || currentCharacter(state) === '-') {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
// Scientific notation MUST be followed by an exponent
|
|
if (!parse.isDigit(currentCharacter(state))) {
|
|
throw createSyntaxError(state, 'Digit expected, got "' + currentCharacter(state) + '"')
|
|
}
|
|
|
|
while (parse.isDigit(currentCharacter(state))) {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
|
|
if (parse.isDecimalMark(currentCharacter(state), nextCharacter(state))) {
|
|
throw createSyntaxError(state, 'Digit expected, got "' + currentCharacter(state) + '"')
|
|
}
|
|
} else if (nextCharacter(state) === '.') {
|
|
next(state)
|
|
throw createSyntaxError(state, 'Digit expected, got "' + currentCharacter(state) + '"')
|
|
}
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// check for variables, functions, named operators
|
|
if (parse.isAlpha(currentCharacter(state), prevCharacter(state), nextCharacter(state))) {
|
|
while (parse.isAlpha(currentCharacter(state), prevCharacter(state), nextCharacter(state)) || parse.isDigit(currentCharacter(state))) {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
|
|
if (NAMED_DELIMITERS.hasOwnProperty(state.token)) {
|
|
state.tokenType = TOKENTYPE.DELIMITER
|
|
} else {
|
|
state.tokenType = TOKENTYPE.SYMBOL
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// something unknown is found, wrong characters -> a syntax error
|
|
state.tokenType = TOKENTYPE.UNKNOWN
|
|
while (currentCharacter(state) !== '') {
|
|
state.token += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
throw createSyntaxError(state, 'Syntax error in part "' + state.token + '"')
|
|
}
|
|
|
|
/**
|
|
* Get next token and skip newline tokens
|
|
*/
|
|
function getTokenSkipNewline (state) {
|
|
do {
|
|
getToken(state)
|
|
}
|
|
while (state.token === '\n') // eslint-disable-line no-unmodified-loop-condition
|
|
}
|
|
|
|
/**
|
|
* Open parameters.
|
|
* New line characters will be ignored until closeParams(state) is called
|
|
*/
|
|
function openParams (state) {
|
|
state.nestingLevel++
|
|
}
|
|
|
|
/**
|
|
* Close parameters.
|
|
* New line characters will no longer be ignored
|
|
*/
|
|
function closeParams (state) {
|
|
state.nestingLevel--
|
|
}
|
|
|
|
/**
|
|
* Checks whether the current character `c` is a valid alpha character:
|
|
*
|
|
* - A latin letter (upper or lower case) Ascii: a-z, A-Z
|
|
* - An underscore Ascii: _
|
|
* - A dollar sign Ascii: $
|
|
* - A latin letter with accents Unicode: \u00C0 - \u02AF
|
|
* - A greek letter Unicode: \u0370 - \u03FF
|
|
* - A mathematical alphanumeric symbol Unicode: \u{1D400} - \u{1D7FF} excluding invalid code points
|
|
*
|
|
* The previous and next characters are needed to determine whether
|
|
* this character is part of a unicode surrogate pair.
|
|
*
|
|
* @param {string} c Current character in the expression
|
|
* @param {string} cPrev Previous character
|
|
* @param {string} cNext Next character
|
|
* @return {boolean}
|
|
*/
|
|
parse.isAlpha = function isAlpha (c, cPrev, cNext) {
|
|
return parse.isValidLatinOrGreek(c) ||
|
|
parse.isValidMathSymbol(c, cNext) ||
|
|
parse.isValidMathSymbol(cPrev, c)
|
|
}
|
|
|
|
/**
|
|
* Test whether a character is a valid latin, greek, or letter-like character
|
|
* @param {string} c
|
|
* @return {boolean}
|
|
*/
|
|
parse.isValidLatinOrGreek = function isValidLatinOrGreek (c) {
|
|
return /^[a-zA-Z_$\u00C0-\u02AF\u0370-\u03FF\u2100-\u214F]$/.test(c)
|
|
}
|
|
|
|
/**
|
|
* Test whether two given 16 bit characters form a surrogate pair of a
|
|
* unicode math symbol.
|
|
*
|
|
* https://unicode-table.com/en/
|
|
* https://www.wikiwand.com/en/Mathematical_operators_and_symbols_in_Unicode
|
|
*
|
|
* Note: In ES6 will be unicode aware:
|
|
* https://stackoverflow.com/questions/280712/javascript-unicode-regexes
|
|
* https://mathiasbynens.be/notes/es6-unicode-regex
|
|
*
|
|
* @param {string} high
|
|
* @param {string} low
|
|
* @return {boolean}
|
|
*/
|
|
parse.isValidMathSymbol = function isValidMathSymbol (high, low) {
|
|
return /^[\uD835]$/.test(high) &&
|
|
/^[\uDC00-\uDFFF]$/.test(low) &&
|
|
/^[^\uDC55\uDC9D\uDCA0\uDCA1\uDCA3\uDCA4\uDCA7\uDCA8\uDCAD\uDCBA\uDCBC\uDCC4\uDD06\uDD0B\uDD0C\uDD15\uDD1D\uDD3A\uDD3F\uDD45\uDD47-\uDD49\uDD51\uDEA6\uDEA7\uDFCC\uDFCD]$/.test(low)
|
|
}
|
|
|
|
/**
|
|
* Check whether given character c is a white space character: space, tab, or enter
|
|
* @param {string} c
|
|
* @param {number} nestingLevel
|
|
* @return {boolean}
|
|
*/
|
|
parse.isWhitespace = function isWhitespace (c, nestingLevel) {
|
|
// TODO: also take '\r' carriage return as newline? Or does that give problems on mac?
|
|
return c === ' ' || c === '\t' || (c === '\n' && nestingLevel > 0)
|
|
}
|
|
|
|
/**
|
|
* Test whether the character c is a decimal mark (dot).
|
|
* This is the case when it's not the start of a delimiter '.*', './', or '.^'
|
|
* @param {string} c
|
|
* @param {string} cNext
|
|
* @return {boolean}
|
|
*/
|
|
parse.isDecimalMark = function isDecimalMark (c, cNext) {
|
|
return c === '.' && cNext !== '/' && cNext !== '*' && cNext !== '^'
|
|
}
|
|
|
|
/**
|
|
* checks if the given char c is a digit or dot
|
|
* @param {string} c a string with one character
|
|
* @return {boolean}
|
|
*/
|
|
parse.isDigitDot = function isDigitDot (c) {
|
|
return ((c >= '0' && c <= '9') || c === '.')
|
|
}
|
|
|
|
/**
|
|
* checks if the given char c is a digit
|
|
* @param {string} c a string with one character
|
|
* @return {boolean}
|
|
*/
|
|
parse.isDigit = function isDigit (c) {
|
|
return (c >= '0' && c <= '9')
|
|
}
|
|
|
|
/**
|
|
* Start of the parse levels below, in order of precedence
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseStart (expression, extraNodes) {
|
|
const state = initialState()
|
|
Object.assign(state, { expression, extraNodes })
|
|
getToken(state)
|
|
|
|
const node = parseBlock(state)
|
|
|
|
// check for garbage at the end of the expression
|
|
// an expression ends with a empty character '' and tokenType DELIMITER
|
|
if (state.token !== '') {
|
|
if (state.tokenType === TOKENTYPE.DELIMITER) {
|
|
// user entered a not existing operator like "//"
|
|
|
|
// TODO: give hints for aliases, for example with "<>" give as hint " did you mean !== ?"
|
|
throw createError(state, 'Unexpected operator ' + state.token)
|
|
} else {
|
|
throw createSyntaxError(state, 'Unexpected part "' + state.token + '"')
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Parse a block with expressions. Expressions can be separated by a newline
|
|
* character '\n', or by a semicolon ';'. In case of a semicolon, no output
|
|
* of the preceding line is returned.
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBlock (state) {
|
|
let node
|
|
const blocks = []
|
|
let visible
|
|
|
|
if (state.token !== '' && state.token !== '\n' && state.token !== ';') {
|
|
node = parseAssignment(state)
|
|
node.comment = state.comment
|
|
}
|
|
|
|
// TODO: simplify this loop
|
|
while (state.token === '\n' || state.token === ';') { // eslint-disable-line no-unmodified-loop-condition
|
|
if (blocks.length === 0 && node) {
|
|
visible = (state.token !== ';')
|
|
blocks.push({
|
|
node: node,
|
|
visible: visible
|
|
})
|
|
}
|
|
|
|
getToken(state)
|
|
if (state.token !== '\n' && state.token !== ';' && state.token !== '') {
|
|
node = parseAssignment(state)
|
|
node.comment = state.comment
|
|
|
|
visible = (state.token !== ';')
|
|
blocks.push({
|
|
node: node,
|
|
visible: visible
|
|
})
|
|
}
|
|
}
|
|
|
|
if (blocks.length > 0) {
|
|
return new BlockNode(blocks)
|
|
} else {
|
|
if (!node) {
|
|
node = new ConstantNode(undefined)
|
|
node.comment = state.comment
|
|
}
|
|
|
|
return node
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Assignment of a function or variable,
|
|
* - can be a variable like 'a=2.3'
|
|
* - or a updating an existing variable like 'matrix(2,3:5)=[6,7,8]'
|
|
* - defining a function like 'f(x) = x^2'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseAssignment (state) {
|
|
let name, args, value, valid
|
|
|
|
const node = parseConditional(state)
|
|
|
|
if (state.token === '=') {
|
|
if (isSymbolNode(node)) {
|
|
// parse a variable assignment like 'a = 2/3'
|
|
name = node.name
|
|
getTokenSkipNewline(state)
|
|
value = parseAssignment(state)
|
|
return new AssignmentNode(new SymbolNode(name), value)
|
|
} else if (isAccessorNode(node)) {
|
|
// parse a matrix subset assignment like 'A[1,2] = 4'
|
|
getTokenSkipNewline(state)
|
|
value = parseAssignment(state)
|
|
return new AssignmentNode(node.object, node.index, value)
|
|
} else if (isFunctionNode(node) && isSymbolNode(node.fn)) {
|
|
// parse function assignment like 'f(x) = x^2'
|
|
valid = true
|
|
args = []
|
|
|
|
name = node.name
|
|
node.args.forEach(function (arg, index) {
|
|
if (isSymbolNode(arg)) {
|
|
args[index] = arg.name
|
|
} else {
|
|
valid = false
|
|
}
|
|
})
|
|
|
|
if (valid) {
|
|
getTokenSkipNewline(state)
|
|
value = parseAssignment(state)
|
|
return new FunctionAssignmentNode(name, args, value)
|
|
}
|
|
}
|
|
|
|
throw createSyntaxError(state, 'Invalid left hand side of assignment operator =')
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* conditional operation
|
|
*
|
|
* condition ? truePart : falsePart
|
|
*
|
|
* Note: conditional operator is right-associative
|
|
*
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseConditional (state) {
|
|
let node = parseLogicalOr(state)
|
|
|
|
while (state.token === '?') { // eslint-disable-line no-unmodified-loop-condition
|
|
// set a conditional level, the range operator will be ignored as long
|
|
// as conditionalLevel === state.nestingLevel.
|
|
const prev = state.conditionalLevel
|
|
state.conditionalLevel = state.nestingLevel
|
|
getTokenSkipNewline(state)
|
|
|
|
const condition = node
|
|
const trueExpr = parseAssignment(state)
|
|
|
|
if (state.token !== ':') throw createSyntaxError(state, 'False part of conditional expression expected')
|
|
|
|
state.conditionalLevel = null
|
|
getTokenSkipNewline(state)
|
|
|
|
const falseExpr = parseAssignment(state) // Note: check for conditional operator again, right associativity
|
|
|
|
node = new ConditionalNode(condition, trueExpr, falseExpr)
|
|
|
|
// restore the previous conditional level
|
|
state.conditionalLevel = prev
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* logical or, 'x or y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLogicalOr (state) {
|
|
let node = parseLogicalXor(state)
|
|
|
|
while (state.token === 'or') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
node = new OperatorNode('or', 'or', [node, parseLogicalXor(state)])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* logical exclusive or, 'x xor y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLogicalXor (state) {
|
|
let node = parseLogicalAnd(state)
|
|
|
|
while (state.token === 'xor') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
node = new OperatorNode('xor', 'xor', [node, parseLogicalAnd(state)])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* logical and, 'x and y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLogicalAnd (state) {
|
|
let node = parseBitwiseOr(state)
|
|
|
|
while (state.token === 'and') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
node = new OperatorNode('and', 'and', [node, parseBitwiseOr(state)])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* bitwise or, 'x | y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBitwiseOr (state) {
|
|
let node = parseBitwiseXor(state)
|
|
|
|
while (state.token === '|') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
node = new OperatorNode('|', 'bitOr', [node, parseBitwiseXor(state)])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* bitwise exclusive or (xor), 'x ^| y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBitwiseXor (state) {
|
|
let node = parseBitwiseAnd(state)
|
|
|
|
while (state.token === '^|') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
node = new OperatorNode('^|', 'bitXor', [node, parseBitwiseAnd(state)])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* bitwise and, 'x & y'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseBitwiseAnd (state) {
|
|
let node = parseRelational(state)
|
|
|
|
while (state.token === '&') { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
node = new OperatorNode('&', 'bitAnd', [node, parseRelational(state)])
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Parse a chained conditional, like 'a > b >= c'
|
|
* @return {Node} node
|
|
*/
|
|
function parseRelational (state) {
|
|
const params = [parseShift(state)]
|
|
const conditionals = []
|
|
|
|
const operators = {
|
|
'==': 'equal',
|
|
'!=': 'unequal',
|
|
'<': 'smaller',
|
|
'>': 'larger',
|
|
'<=': 'smallerEq',
|
|
'>=': 'largerEq'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(state.token)) { // eslint-disable-line no-unmodified-loop-condition
|
|
const cond = { name: state.token, fn: operators[state.token] }
|
|
conditionals.push(cond)
|
|
getTokenSkipNewline(state)
|
|
params.push(parseShift(state))
|
|
}
|
|
|
|
if (params.length === 1) {
|
|
return params[0]
|
|
} else if (params.length === 2) {
|
|
return new OperatorNode(conditionals[0].name, conditionals[0].fn, params)
|
|
} else {
|
|
return new RelationalNode(conditionals.map(c => c.fn), params)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Bitwise left shift, bitwise right arithmetic shift, bitwise right logical shift
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseShift (state) {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseConversion(state)
|
|
|
|
operators = {
|
|
'<<': 'leftShift',
|
|
'>>': 'rightArithShift',
|
|
'>>>': 'rightLogShift'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(state.token)) {
|
|
name = state.token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline(state)
|
|
params = [node, parseConversion(state)]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* conversion operators 'to' and 'in'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseConversion (state) {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseRange(state)
|
|
|
|
operators = {
|
|
'to': 'to',
|
|
'in': 'to' // alias of 'to'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(state.token)) {
|
|
name = state.token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline(state)
|
|
|
|
if (name === 'in' && state.token === '') {
|
|
// end of expression -> this is the unit 'in' ('inch')
|
|
node = new OperatorNode('*', 'multiply', [node, new SymbolNode('in')], true)
|
|
} else {
|
|
// operator 'a to b' or 'a in b'
|
|
params = [node, parseRange(state)]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* parse range, "start:end", "start:step:end", ":", "start:", ":end", etc
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseRange (state) {
|
|
let node
|
|
const params = []
|
|
|
|
if (state.token === ':') {
|
|
// implicit start=1 (one-based)
|
|
node = new ConstantNode(1)
|
|
} else {
|
|
// explicit start
|
|
node = parseAddSubtract(state)
|
|
}
|
|
|
|
if (state.token === ':' && (state.conditionalLevel !== state.nestingLevel)) {
|
|
// we ignore the range operator when a conditional operator is being processed on the same level
|
|
params.push(node)
|
|
|
|
// parse step and end
|
|
while (state.token === ':' && params.length < 3) { // eslint-disable-line no-unmodified-loop-condition
|
|
getTokenSkipNewline(state)
|
|
|
|
if (state.token === ')' || state.token === ']' || state.token === ',' || state.token === '') {
|
|
// implicit end
|
|
params.push(new SymbolNode('end'))
|
|
} else {
|
|
// explicit end
|
|
params.push(parseAddSubtract(state))
|
|
}
|
|
}
|
|
|
|
if (params.length === 3) {
|
|
// params = [start, step, end]
|
|
node = new RangeNode(params[0], params[2], params[1]) // start, end, step
|
|
} else { // length === 2
|
|
// params = [start, end]
|
|
node = new RangeNode(params[0], params[1]) // start, end
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* add or subtract
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseAddSubtract (state) {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseMultiplyDivide(state)
|
|
|
|
operators = {
|
|
'+': 'add',
|
|
'-': 'subtract'
|
|
}
|
|
while (operators.hasOwnProperty(state.token)) {
|
|
name = state.token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline(state)
|
|
params = [node, parseMultiplyDivide(state)]
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* multiply, divide, modulus
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseMultiplyDivide (state) {
|
|
let node, last, operators, name, fn
|
|
|
|
node = parseImplicitMultiplication(state)
|
|
last = node
|
|
|
|
operators = {
|
|
'*': 'multiply',
|
|
'.*': 'dotMultiply',
|
|
'/': 'divide',
|
|
'./': 'dotDivide',
|
|
'%': 'mod',
|
|
'mod': 'mod'
|
|
}
|
|
|
|
while (true) {
|
|
if (operators.hasOwnProperty(state.token)) {
|
|
// explicit operators
|
|
name = state.token
|
|
fn = operators[name]
|
|
|
|
getTokenSkipNewline(state)
|
|
|
|
last = parseImplicitMultiplication(state)
|
|
node = new OperatorNode(name, fn, [node, last])
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* implicit multiplication
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseImplicitMultiplication (state) {
|
|
let node, last
|
|
|
|
node = parseRule2(state)
|
|
last = node
|
|
|
|
while (true) {
|
|
if ((state.tokenType === TOKENTYPE.SYMBOL) ||
|
|
(state.token === 'in' && isConstantNode(node)) ||
|
|
(state.tokenType === TOKENTYPE.NUMBER &&
|
|
!isConstantNode(last) &&
|
|
(!isOperatorNode(last) || last.op === '!')) ||
|
|
(state.token === '(')) {
|
|
// parse implicit multiplication
|
|
//
|
|
// symbol: implicit multiplication like '2a', '(2+3)a', 'a b'
|
|
// number: implicit multiplication like '(2+3)2'
|
|
// parenthesis: implicit multiplication like '2(3+4)', '(3+4)(1+2)'
|
|
last = parseRule2(state)
|
|
node = new OperatorNode('*', 'multiply', [node, last], true /* implicit */)
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Infamous "rule 2" as described in https://github.com/josdejong/mathjs/issues/792#issuecomment-361065370
|
|
* Explicit division gets higher precedence than implicit multiplication
|
|
* when the division matches this pattern: [number] / [number] [symbol]
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseRule2 (state) {
|
|
let node = parseUnary(state)
|
|
let last = node
|
|
let tokenStates = []
|
|
|
|
while (true) {
|
|
// Match the "number /" part of the pattern "number / number symbol"
|
|
if (state.token === '/' && isConstantNode(last)) {
|
|
// Look ahead to see if the next token is a number
|
|
tokenStates.push(Object.assign({}, state))
|
|
getTokenSkipNewline(state)
|
|
|
|
// Match the "number / number" part of the pattern
|
|
if (state.tokenType === TOKENTYPE.NUMBER) {
|
|
// Look ahead again
|
|
tokenStates.push(Object.assign({}, state))
|
|
getTokenSkipNewline(state)
|
|
|
|
// Match the "symbol" part of the pattern, or a left parenthesis
|
|
if (state.tokenType === TOKENTYPE.SYMBOL || state.token === '(') {
|
|
// We've matched the pattern "number / number symbol".
|
|
// Rewind once and build the "number / number" node; the symbol will be consumed later
|
|
Object.assign(state, tokenStates.pop())
|
|
tokenStates.pop()
|
|
last = parseUnary(state)
|
|
node = new OperatorNode('/', 'divide', [node, last])
|
|
} else {
|
|
// Not a match, so rewind
|
|
tokenStates.pop()
|
|
Object.assign(state, tokenStates.pop())
|
|
break
|
|
}
|
|
} else {
|
|
// Not a match, so rewind
|
|
Object.assign(state, tokenStates.pop())
|
|
break
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Unary plus and minus, and logical and bitwise not
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseUnary (state) {
|
|
let name, params, fn
|
|
const operators = {
|
|
'-': 'unaryMinus',
|
|
'+': 'unaryPlus',
|
|
'~': 'bitNot',
|
|
'not': 'not'
|
|
}
|
|
|
|
if (operators.hasOwnProperty(state.token)) {
|
|
fn = operators[state.token]
|
|
name = state.token
|
|
|
|
getTokenSkipNewline(state)
|
|
params = [parseUnary(state)]
|
|
|
|
return new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return parsePow(state)
|
|
}
|
|
|
|
/**
|
|
* power
|
|
* Note: power operator is right associative
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parsePow (state) {
|
|
let node, name, fn, params
|
|
|
|
node = parseLeftHandOperators(state)
|
|
|
|
if (state.token === '^' || state.token === '.^') {
|
|
name = state.token
|
|
fn = (name === '^') ? 'pow' : 'dotPow'
|
|
|
|
getTokenSkipNewline(state)
|
|
params = [node, parseUnary(state)] // Go back to unary, we can have '2^-3'
|
|
node = new OperatorNode(name, fn, params)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Left hand operators: factorial x!, ctranspose x'
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseLeftHandOperators (state) {
|
|
let node, operators, name, fn, params
|
|
|
|
node = parseCustomNodes(state)
|
|
|
|
operators = {
|
|
'!': 'factorial',
|
|
'\'': 'ctranspose'
|
|
}
|
|
|
|
while (operators.hasOwnProperty(state.token)) {
|
|
name = state.token
|
|
fn = operators[name]
|
|
|
|
getToken(state)
|
|
params = [node]
|
|
|
|
node = new OperatorNode(name, fn, params)
|
|
node = parseAccessors(state, node)
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Parse a custom node handler. A node handler can be used to process
|
|
* nodes in a custom way, for example for handling a plot.
|
|
*
|
|
* A handler must be passed as second argument of the parse function.
|
|
* - must extend math.expression.node.Node
|
|
* - must contain a function _compile(defs: Object) : string
|
|
* - must contain a function find(filter: Object) : Node[]
|
|
* - must contain a function toString() : string
|
|
* - the constructor is called with a single argument containing all parameters
|
|
*
|
|
* For example:
|
|
*
|
|
* nodes = {
|
|
* 'plot': PlotHandler
|
|
* }
|
|
*
|
|
* The constructor of the handler is called as:
|
|
*
|
|
* node = new PlotHandler(params)
|
|
*
|
|
* The handler will be invoked when evaluating an expression like:
|
|
*
|
|
* node = math.parse('plot(sin(x), x)', nodes)
|
|
*
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseCustomNodes (state) {
|
|
let params = []
|
|
|
|
if (state.tokenType === TOKENTYPE.SYMBOL && state.extraNodes.hasOwnProperty(state.token)) {
|
|
const CustomNode = state.extraNodes[state.token]
|
|
|
|
getToken(state)
|
|
|
|
// parse parameters
|
|
if (state.token === '(') {
|
|
params = []
|
|
|
|
openParams(state)
|
|
getToken(state)
|
|
|
|
if (state.token !== ')') {
|
|
params.push(parseAssignment(state))
|
|
|
|
// parse a list with parameters
|
|
while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken(state)
|
|
params.push(parseAssignment(state))
|
|
}
|
|
}
|
|
|
|
if (state.token !== ')') {
|
|
throw createSyntaxError(state, 'Parenthesis ) expected')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
}
|
|
|
|
// create a new custom node
|
|
// noinspection JSValidateTypes
|
|
return new CustomNode(params)
|
|
}
|
|
|
|
return parseSymbol(state)
|
|
}
|
|
|
|
/**
|
|
* parse symbols: functions, variables, constants, units
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseSymbol (state) {
|
|
let node, name
|
|
|
|
if (state.tokenType === TOKENTYPE.SYMBOL ||
|
|
(state.tokenType === TOKENTYPE.DELIMITER && state.token in NAMED_DELIMITERS)) {
|
|
name = state.token
|
|
|
|
getToken(state)
|
|
|
|
if (CONSTANTS.hasOwnProperty(name)) { // true, false, null, ...
|
|
node = new ConstantNode(CONSTANTS[name])
|
|
} else if (NUMERIC_CONSTANTS.indexOf(name) !== -1) { // NaN, Infinity
|
|
node = new ConstantNode(numeric(name, 'number'))
|
|
} else {
|
|
node = new SymbolNode(name)
|
|
}
|
|
|
|
// parse function parameters and matrix index
|
|
node = parseAccessors(state, node)
|
|
return node
|
|
}
|
|
|
|
return parseDoubleQuotesString(state)
|
|
}
|
|
|
|
/**
|
|
* parse accessors:
|
|
* - function invocation in round brackets (...), for example sqrt(2)
|
|
* - index enclosed in square brackets [...], for example A[2,3]
|
|
* - dot notation for properties, like foo.bar
|
|
* @param {Node} node Node on which to apply the parameters. If there
|
|
* are no parameters in the expression, the node
|
|
* itself is returned
|
|
* @param {string[]} [types] Filter the types of notations
|
|
* can be ['(', '[', '.']
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseAccessors (state, node, types) {
|
|
let params
|
|
|
|
while ((state.token === '(' || state.token === '[' || state.token === '.') &&
|
|
(!types || types.indexOf(state.token) !== -1)) { // eslint-disable-line no-unmodified-loop-condition
|
|
params = []
|
|
|
|
if (state.token === '(') {
|
|
if (isSymbolNode(node) || isAccessorNode(node)) {
|
|
// function invocation like fn(2, 3) or obj.fn(2, 3)
|
|
openParams(state)
|
|
getToken(state)
|
|
|
|
if (state.token !== ')') {
|
|
params.push(parseAssignment(state))
|
|
|
|
// parse a list with parameters
|
|
while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken(state)
|
|
params.push(parseAssignment(state))
|
|
}
|
|
}
|
|
|
|
if (state.token !== ')') {
|
|
throw createSyntaxError(state, 'Parenthesis ) expected')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
|
|
node = new FunctionNode(node, params)
|
|
} else {
|
|
// implicit multiplication like (2+3)(4+5) or sqrt(2)(1+2)
|
|
// don't parse it here but let it be handled by parseImplicitMultiplication
|
|
// with correct precedence
|
|
return node
|
|
}
|
|
} else if (state.token === '[') {
|
|
// index notation like variable[2, 3]
|
|
openParams(state)
|
|
getToken(state)
|
|
|
|
if (state.token !== ']') {
|
|
params.push(parseAssignment(state))
|
|
|
|
// parse a list with parameters
|
|
while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken(state)
|
|
params.push(parseAssignment(state))
|
|
}
|
|
}
|
|
|
|
if (state.token !== ']') {
|
|
throw createSyntaxError(state, 'Parenthesis ] expected')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
|
|
node = new AccessorNode(node, new IndexNode(params))
|
|
} else {
|
|
// dot notation like variable.prop
|
|
getToken(state)
|
|
|
|
if (state.tokenType !== TOKENTYPE.SYMBOL) {
|
|
throw createSyntaxError(state, 'Property name expected after dot')
|
|
}
|
|
params.push(new ConstantNode(state.token))
|
|
getToken(state)
|
|
|
|
const dotNotation = true
|
|
node = new AccessorNode(node, new IndexNode(params, dotNotation))
|
|
}
|
|
}
|
|
|
|
return node
|
|
}
|
|
|
|
/**
|
|
* Parse a double quotes string.
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseDoubleQuotesString (state) {
|
|
let node, str
|
|
|
|
if (state.token === '"') {
|
|
str = parseDoubleQuotesStringToken(state)
|
|
|
|
// create constant
|
|
node = new ConstantNode(str)
|
|
|
|
// parse index parameters
|
|
node = parseAccessors(state, node)
|
|
|
|
return node
|
|
}
|
|
|
|
return parseSingleQuotesString(state)
|
|
}
|
|
|
|
/**
|
|
* Parse a string surrounded by double quotes "..."
|
|
* @return {string}
|
|
*/
|
|
function parseDoubleQuotesStringToken (state) {
|
|
let str = ''
|
|
|
|
while (currentCharacter(state) !== '' && currentCharacter(state) !== '"') {
|
|
if (currentCharacter(state) === '\\') {
|
|
// escape character, immediately process the next
|
|
// character to prevent stopping at a next '\"'
|
|
str += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
|
|
str += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
|
|
getToken(state)
|
|
if (state.token !== '"') {
|
|
throw createSyntaxError(state, 'End of string " expected')
|
|
}
|
|
getToken(state)
|
|
|
|
return JSON.parse('"' + str + '"') // unescape escaped characters
|
|
}
|
|
|
|
/**
|
|
* Parse a single quotes string.
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseSingleQuotesString (state) {
|
|
let node, str
|
|
|
|
if (state.token === '\'') {
|
|
str = parseSingleQuotesStringToken(state)
|
|
|
|
// create constant
|
|
node = new ConstantNode(str)
|
|
|
|
// parse index parameters
|
|
node = parseAccessors(state, node)
|
|
|
|
return node
|
|
}
|
|
|
|
return parseMatrix(state)
|
|
}
|
|
|
|
/**
|
|
* Parse a string surrounded by single quotes '...'
|
|
* @return {string}
|
|
*/
|
|
function parseSingleQuotesStringToken (state) {
|
|
let str = ''
|
|
|
|
while (currentCharacter(state) !== '' && currentCharacter(state) !== '\'') {
|
|
if (currentCharacter(state) === '\\') {
|
|
// escape character, immediately process the next
|
|
// character to prevent stopping at a next '\''
|
|
str += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
|
|
str += currentCharacter(state)
|
|
next(state)
|
|
}
|
|
|
|
getToken(state)
|
|
if (state.token !== '\'') {
|
|
throw createSyntaxError(state, 'End of string \' expected')
|
|
}
|
|
getToken(state)
|
|
|
|
return JSON.parse('"' + str + '"') // unescape escaped characters
|
|
}
|
|
|
|
/**
|
|
* parse the matrix
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseMatrix (state) {
|
|
let array, params, rows, cols
|
|
|
|
if (state.token === '[') {
|
|
// matrix [...]
|
|
openParams(state)
|
|
getToken(state)
|
|
|
|
if (state.token !== ']') {
|
|
// this is a non-empty matrix
|
|
const row = parseRow(state)
|
|
|
|
if (state.token === ';') {
|
|
// 2 dimensional array
|
|
rows = 1
|
|
params = [row]
|
|
|
|
// the rows of the matrix are separated by dot-comma's
|
|
while (state.token === ';') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken(state)
|
|
|
|
params[rows] = parseRow(state)
|
|
rows++
|
|
}
|
|
|
|
if (state.token !== ']') {
|
|
throw createSyntaxError(state, 'End of matrix ] expected')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
|
|
// check if the number of columns matches in all rows
|
|
cols = params[0].items.length
|
|
for (let r = 1; r < rows; r++) {
|
|
if (params[r].items.length !== cols) {
|
|
throw createError(state, 'Column dimensions mismatch ' +
|
|
'(' + params[r].items.length + ' !== ' + cols + ')')
|
|
}
|
|
}
|
|
|
|
array = new ArrayNode(params)
|
|
} else {
|
|
// 1 dimensional vector
|
|
if (state.token !== ']') {
|
|
throw createSyntaxError(state, 'End of matrix ] expected')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
|
|
array = row
|
|
}
|
|
} else {
|
|
// this is an empty matrix "[ ]"
|
|
closeParams(state)
|
|
getToken(state)
|
|
array = new ArrayNode([])
|
|
}
|
|
|
|
return parseAccessors(state, array)
|
|
}
|
|
|
|
return parseObject(state)
|
|
}
|
|
|
|
/**
|
|
* Parse a single comma-separated row from a matrix, like 'a, b, c'
|
|
* @return {ArrayNode} node
|
|
*/
|
|
function parseRow (state) {
|
|
const params = [parseAssignment(state)]
|
|
let len = 1
|
|
|
|
while (state.token === ',') { // eslint-disable-line no-unmodified-loop-condition
|
|
getToken(state)
|
|
|
|
// parse expression
|
|
params[len] = parseAssignment(state)
|
|
len++
|
|
}
|
|
|
|
return new ArrayNode(params)
|
|
}
|
|
|
|
/**
|
|
* parse an object, enclosed in angle brackets{...}, for example {value: 2}
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseObject (state) {
|
|
if (state.token === '{') {
|
|
openParams(state)
|
|
let key
|
|
|
|
const properties = {}
|
|
do {
|
|
getToken(state)
|
|
|
|
if (state.token !== '}') {
|
|
// parse key
|
|
if (state.token === '"') {
|
|
key = parseDoubleQuotesStringToken(state)
|
|
} else if (state.token === '\'') {
|
|
key = parseSingleQuotesStringToken(state)
|
|
} else if (state.tokenType === TOKENTYPE.SYMBOL) {
|
|
key = state.token
|
|
getToken(state)
|
|
} else {
|
|
throw createSyntaxError(state, 'Symbol or string expected as object key')
|
|
}
|
|
|
|
// parse key/value separator
|
|
if (state.token !== ':') {
|
|
throw createSyntaxError(state, 'Colon : expected after object key')
|
|
}
|
|
getToken(state)
|
|
|
|
// parse key
|
|
properties[key] = parseAssignment(state)
|
|
}
|
|
}
|
|
while (state.token === ',') // eslint-disable-line no-unmodified-loop-condition
|
|
|
|
if (state.token !== '}') {
|
|
throw createSyntaxError(state, 'Comma , or bracket } expected after object value')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
|
|
let node = new ObjectNode(properties)
|
|
|
|
// parse index parameters
|
|
node = parseAccessors(state, node)
|
|
|
|
return node
|
|
}
|
|
|
|
return parseNumber(state)
|
|
}
|
|
|
|
/**
|
|
* parse a number
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseNumber (state) {
|
|
let numberStr
|
|
|
|
if (state.tokenType === TOKENTYPE.NUMBER) {
|
|
// this is a number
|
|
numberStr = state.token
|
|
getToken(state)
|
|
|
|
return new ConstantNode(numeric(numberStr, config.number))
|
|
}
|
|
|
|
return parseParentheses(state)
|
|
}
|
|
|
|
/**
|
|
* parentheses
|
|
* @return {Node} node
|
|
* @private
|
|
*/
|
|
function parseParentheses (state) {
|
|
let node
|
|
|
|
// check if it is a parenthesized expression
|
|
if (state.token === '(') {
|
|
// parentheses (...)
|
|
openParams(state)
|
|
getToken(state)
|
|
|
|
node = parseAssignment(state) // start again
|
|
|
|
if (state.token !== ')') {
|
|
throw createSyntaxError(state, 'Parenthesis ) expected')
|
|
}
|
|
closeParams(state)
|
|
getToken(state)
|
|
|
|
node = new ParenthesisNode(node)
|
|
node = parseAccessors(state, node)
|
|
return node
|
|
}
|
|
|
|
return parseEnd(state)
|
|
}
|
|
|
|
/**
|
|
* Evaluated when the expression is not yet ended but expected to end
|
|
* @return {Node} res
|
|
* @private
|
|
*/
|
|
function parseEnd (state) {
|
|
if (state.token === '') {
|
|
// syntax error or unexpected end of expression
|
|
throw createSyntaxError(state, 'Unexpected end of expression')
|
|
} else {
|
|
throw createSyntaxError(state, 'Value expected')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shortcut for getting the current row value (one based)
|
|
* Returns the line of the currently handled expression
|
|
* @private
|
|
*/
|
|
/* TODO: implement keeping track on the row number
|
|
function row () {
|
|
return null
|
|
}
|
|
*/
|
|
|
|
/**
|
|
* Shortcut for getting the current col value (one based)
|
|
* Returns the column (position) where the last state.token starts
|
|
* @private
|
|
*/
|
|
function col (state) {
|
|
return state.index - state.token.length + 1
|
|
}
|
|
|
|
/**
|
|
* Create an error
|
|
* @param {string} message
|
|
* @return {SyntaxError} instantiated error
|
|
* @private
|
|
*/
|
|
function createSyntaxError (state, message) {
|
|
const c = col(state)
|
|
const error = new SyntaxError(message + ' (char ' + c + ')')
|
|
error['char'] = c
|
|
|
|
return error
|
|
}
|
|
|
|
/**
|
|
* Create an error
|
|
* @param {string} message
|
|
* @return {Error} instantiated error
|
|
* @private
|
|
*/
|
|
function createError (state, message) {
|
|
const c = col(state)
|
|
const error = new SyntaxError(message + ' (char ' + c + ')')
|
|
error['char'] = c
|
|
|
|
return error
|
|
}
|
|
|
|
return parse
|
|
})
|