LogarithmPlotter/common/src/lib/expr-eval/expression.mjs
Ad5001 fbef5dc28a
All checks were successful
continuous-integration/drone/push Build is passing
Updating copyrights to 2025.
2025-02-06 19:14:00 +01:00

524 lines
No EOL
19 KiB
JavaScript

/**
* Based on ndef.parser, by Raphael Graf <r@undefined.ch>
* http://www.undefined.ch/mparser/index.html
*
* Ported to JavaScript and modified by Matthew Crumley <email@matthewcrumley.com>
* https://silentmatt.com/javascript-expression-evaluator/
*
* Ported to QMLJS with modifications done accordingly done by Ad5001 <mail@ad5001.eu> (https://ad5001.eu)
*
* Copyright (c) 2015 Matthew Crumley, 2021-2025 Ad5001
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*
* You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
* to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
* but don't feel like you have to let me know or ask permission.
*/
import {
Instruction,
IOP3, IOP2, IOP1,
INUMBER, IARRAY,
IVAR, IVARNAME,
IEXPR, IEXPREVAL,
IMEMBER, IFUNCALL,
IENDSTATEMENT,
unaryInstruction, binaryInstruction, ternaryInstruction
} from "./instruction.mjs"
/**
* Simplifies the given instructions
* @param {Instruction[]} tokens
* @param {Record.<string, function(any): any>} unaryOps
* @param {Record.<string, function(any, any): any>} binaryOps
* @param {Record.<string, function(any, any, any): any>} ternaryOps
* @param {Record.<string, any>} values
* @return {Instruction[]}
*/
function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) {
const nstack = []
const newexpression = []
let n1, n2, n3
let f
for(let i = 0; i < tokens.length; i++) {
let item = tokens[i]
const type = item.type
if(type === INUMBER || type === IVARNAME) {
if(Array.isArray(item.value)) {
nstack.push.apply(nstack, simplify(item.value.map(function(x) {
return new Instruction(INUMBER, x)
}).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values))
} else {
nstack.push(item)
}
} else if(type === IVAR && values.hasOwnProperty(item.value)) {
item = new Instruction(INUMBER, values[item.value])
nstack.push(item)
} else if(type === IOP2 && nstack.length > 1) {
n2 = nstack.pop()
n1 = nstack.pop()
f = binaryOps[item.value]
item = new Instruction(INUMBER, f(n1.value, n2.value))
nstack.push(item)
} else if(type === IOP3 && nstack.length > 2) {
n3 = nstack.pop()
n2 = nstack.pop()
n1 = nstack.pop()
if(item.value === "?") {
nstack.push(n1.value ? n2.value : n3.value)
} else {
f = ternaryOps[item.value]
item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value))
nstack.push(item)
}
} else if(type === IOP1 && nstack.length > 0) {
n1 = nstack.pop()
f = unaryOps[item.value]
item = new Instruction(INUMBER, f(n1.value))
nstack.push(item)
} else if(type === IEXPR) {
while(nstack.length > 0) {
newexpression.push(nstack.shift())
}
newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values)))
} else if(type === IMEMBER && nstack.length > 0) {
n1 = nstack.pop()
if(item.value in n1.value)
nstack.push(new Instruction(INUMBER, n1.value[item.value]))
else
throw new Error(qsTranslate("error", "Cannot find property %1 of object %2.").arg(item.value).arg(n1))
} else {
while(nstack.length > 0) {
newexpression.push(nstack.shift())
}
newexpression.push(item)
}
}
while(nstack.length > 0) {
newexpression.push(nstack.shift())
}
return newexpression
}
/**
* In the given instructions, replaces variable by expr.
* @param {Instruction[]} tokens
* @param {string} variable
* @param {ExprEvalExpression} expr
* @return {Instruction[]}
*/
function substitute(tokens, variable, expr) {
const newexpression = []
for(let i = 0; i < tokens.length; i++) {
let item = tokens[i]
const type = item.type
if(type === IVAR && item.value === variable) {
for(let j = 0; j < expr.tokens.length; j++) {
const expritem = expr.tokens[j]
let replitem
if(expritem.type === IOP1) {
replitem = unaryInstruction(expritem.value)
} else if(expritem.type === IOP2) {
replitem = binaryInstruction(expritem.value)
} else if(expritem.type === IOP3) {
replitem = ternaryInstruction(expritem.value)
} else {
replitem = new Instruction(expritem.type, expritem.value)
}
newexpression.push(replitem)
}
} else if(type === IEXPR) {
newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr)))
} else {
newexpression.push(item)
}
}
return newexpression
}
/**
* Evaluates the given instructions for a given Expression with given values.
* @param {Instruction[]} tokens
* @param {ExprEvalExpression} expr
* @param {Record.<string, number>} values
* @return {number}
*/
function evaluate(tokens, expr, values) {
const nstack = []
let n1, n2, n3
let f, args, argCount
if(isExpressionEvaluator(tokens)) {
return resolveExpression(tokens, values)
}
for(let i = 0; i < tokens.length; i++) {
const item = tokens[i]
const type = item.type
if(type === INUMBER || type === IVARNAME) {
nstack.push(item.value)
} else if(type === IOP2) {
n2 = nstack.pop()
n1 = nstack.pop()
if(item.value === "and") {
nstack.push(n1 ? !!evaluate(n2, expr, values) : false)
} else if(item.value === "or") {
nstack.push(n1 ? true : !!evaluate(n2, expr, values))
} else {
f = expr.binaryOps[item.value]
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values)))
}
} else if(type === IOP3) {
n3 = nstack.pop()
n2 = nstack.pop()
n1 = nstack.pop()
if(item.value === "?") {
nstack.push(evaluate(n1 ? n2 : n3, expr, values))
} else {
f = expr.ternaryOps[item.value]
nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values)))
}
} else if(type === IVAR) {
// Check for variable value
if(/^__proto__|prototype|constructor$/.test(item.value)) {
throw new Error("WARNING: Prototype access detected and denied. If you downloaded this file from the internet, this file might be a virus.")
} else if(item.value in expr.functions) {
nstack.push(expr.functions[item.value])
} else if(item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) {
nstack.push(expr.unaryOps[item.value])
} else {
const v = values[item.value]
if(v !== undefined) {
nstack.push(v)
} else {
throw new Error(qsTranslate("error", "Undefined variable %1.").arg(item.value))
}
}
} else if(type === IOP1) {
n1 = nstack.pop()
f = expr.unaryOps[item.value]
nstack.push(f(resolveExpression(n1, values)))
} else if(type === IFUNCALL) {
argCount = item.value
args = []
while(argCount-- > 0) {
args.unshift(resolveExpression(nstack.pop(), values))
}
f = nstack.pop()
if(f.apply && f.call) {
nstack.push(f.apply(undefined, args))
} else if(f.execute) {
// Objects & expressions execution
if(args.length >= 1)
nstack.push(f.execute.apply(f, args))
else
throw new Error(qsTranslate("error", "In order to be executed, object %1 must have at least one argument.").arg(f))
} else {
throw new Error(qsTranslate("error", "%1 cannot be executed.").arg(f))
}
} else if(type === IEXPR) {
nstack.push(createExpressionEvaluator(item, expr))
} else if(type === IEXPREVAL) {
nstack.push(item)
} else if(type === IMEMBER) {
n1 = nstack.pop()
if(item.value in n1)
if(n1[item.value].execute && n1[item.value].cached)
nstack.push(n1[item.value].execute())
else
nstack.push(n1[item.value])
else
throw new Error(qsTranslate("error", "Cannot find property %1 of object %2.").arg(item.value).arg(n1))
} else if(type === IENDSTATEMENT) {
nstack.pop()
} else if(type === IARRAY) {
argCount = item.value
args = []
while(argCount-- > 0) {
args.unshift(nstack.pop())
}
nstack.push(args)
} else {
throw new Error(qsTranslate("error", "Invalid expression."))
}
}
if(nstack.length > 1) {
throw new Error(qsTranslate("error", "Invalid expression (parity)."))
}
// Explicitly return zero to avoid test issues caused by -0
return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values)
}
function createExpressionEvaluator(token, expr) {
if(isExpressionEvaluator(token)) return token
return {
type: IEXPREVAL,
value: function(scope) {
return evaluate(token.value, expr, scope)
}
}
}
function isExpressionEvaluator(n) {
return n && n.type === IEXPREVAL
}
function resolveExpression(n, values) {
return isExpressionEvaluator(n) ? n.value(values) : n
}
/**
* Converts the given instructions to a string
* If toJS is active, can be evaluated with eval, otherwise it can be reparsed by the parser.
* @param {Instruction[]} tokens
* @param {boolean} toJS
* @return {string}
*/
function expressionToString(tokens, toJS) {
let nstack = []
let n1, n2, n3
let f, args, argCount
for(let i = 0; i < tokens.length; i++) {
const item = tokens[i]
const type = item.type
if(type === INUMBER) {
if(typeof item.value === "number" && item.value < 0) {
nstack.push("(" + item.value + ")")
} else if(Array.isArray(item.value)) {
nstack.push("[" + item.value.map(escapeValue).join(", ") + "]")
} else {
nstack.push(escapeValue(item.value))
}
} else if(type === IOP2) {
n2 = nstack.pop()
n1 = nstack.pop()
f = item.value
if(toJS) {
if(f === "^") {
nstack.push("Math.pow(" + n1 + ", " + n2 + ")")
} else if(f === "and") {
nstack.push("(!!" + n1 + " && !!" + n2 + ")")
} else if(f === "or") {
nstack.push("(!!" + n1 + " || !!" + n2 + ")")
} else if(f === "||") {
nstack.push("(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((" + n1 + "),(" + n2 + ")))")
} else if(f === "==") {
nstack.push("(" + n1 + " === " + n2 + ")")
} else if(f === "!=") {
nstack.push("(" + n1 + " !== " + n2 + ")")
} else if(f === "[") {
nstack.push(n1 + "[(" + n2 + ") | 0]")
} else {
nstack.push("(" + n1 + " " + f + " " + n2 + ")")
}
} else {
if(f === "[") {
nstack.push(n1 + "[" + n2 + "]")
} else {
nstack.push("(" + n1 + " " + f + " " + n2 + ")")
}
}
} else if(type === IOP3) {
n3 = nstack.pop()
n2 = nstack.pop()
n1 = nstack.pop()
f = item.value
if(f === "?") {
nstack.push("(" + n1 + " ? " + n2 + " : " + n3 + ")")
} else {
throw new Error(qsTranslate("error", "Invalid expression."))
}
} else if(type === IVAR || type === IVARNAME) {
nstack.push(item.value)
} else if(type === IOP1) {
n1 = nstack.pop()
f = item.value
if(f === "-" || f === "+") {
nstack.push("(" + f + n1 + ")")
} else if(toJS) {
if(f === "not") {
nstack.push("(" + "!" + n1 + ")")
} else if(f === "!") {
nstack.push("fac(" + n1 + ")")
} else {
nstack.push(f + "(" + n1 + ")")
}
} else if(f === "!") {
nstack.push("(" + n1 + "!)")
} else {
nstack.push("(" + f + " " + n1 + ")")
}
} else if(type === IFUNCALL) {
argCount = item.value
args = []
while(argCount-- > 0) {
args.unshift(nstack.pop())
}
f = nstack.pop()
nstack.push(f + "(" + args.join(", ") + ")")
} else if(type === IMEMBER) {
n1 = nstack.pop()
nstack.push(n1 + "." + item.value)
} else if(type === IARRAY) {
argCount = item.value
args = []
while(argCount-- > 0) {
args.unshift(nstack.pop())
}
nstack.push("[" + args.join(", ") + "]")
} else if(type === IEXPR) {
nstack.push("(" + expressionToString(item.value, toJS) + ")")
} else if(type === IENDSTATEMENT) {
} else {
throw new Error(qsTranslate("error", "Invalid expression."))
}
}
if(nstack.length > 1) {
if(toJS) {
nstack = [nstack.join(",")]
} else {
nstack = [nstack.join(";")]
}
}
return String(nstack[0])
}
export function escapeValue(v) {
if(typeof v === "string") {
return JSON.stringify(v).replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029")
}
return v
}
/**
* Pushes all symbols from tokens into the symbols array.
* @param {Instruction[]} tokens
* @param {string[]} symbols
* @param {{withMembers: (boolean|undefined)}}options
*/
function getSymbols(tokens, symbols, options) {
options = options || {}
const withMembers = !!options.withMembers
let prevVar = null
for(let i = 0; i < tokens.length; i++) {
const item = tokens[i]
if(item.type === IVAR || item.type === IVARNAME) {
if(!withMembers && !symbols.includes(item.value)) {
symbols.push(item.value)
} else if(prevVar !== null) {
if(!symbols.includes(prevVar)) {
symbols.push(prevVar)
}
prevVar = item.value
} else {
prevVar = item.value
}
} else if(item.type === IMEMBER && withMembers && prevVar !== null) {
prevVar += "." + item.value
} else if(item.type === IEXPR) {
getSymbols(item.value, symbols, options)
} else if(prevVar !== null) {
if(!symbols.includes(prevVar)) {
symbols.push(prevVar)
}
prevVar = null
}
}
if(prevVar !== null && !symbols.includes(prevVar)) {
symbols.push(prevVar)
}
}
export class ExprEvalExpression {
/**
* @param {Instruction[]} tokens
* @param {Parser} parser
*/
constructor(tokens, parser) {
this.tokens = tokens
this.parser = parser
this.unaryOps = parser.unaryOps
this.binaryOps = parser.binaryOps
this.ternaryOps = parser.ternaryOps
this.functions = parser.functions
}
/**
* Simplifies the expression.
* @param {Object<string, number|ExprEvalExpression>|undefined} values
* @returns {ExprEvalExpression}
*/
simplify(values) {
values = values || {}
return new ExprEvalExpression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser)
}
/**
* Creates a new expression where the variable is substituted by the given expression.
* @param {string} variable
* @param {string|ExprEvalExpression} expr
* @returns {ExprEvalExpression}
*/
substitute(variable, expr) {
if(!(expr instanceof ExprEvalExpression)) {
expr = this.parser.parse(String(expr))
}
return new ExprEvalExpression(substitute(this.tokens, variable, expr), this.parser)
}
/**
* Calculates the value of the expression by giving all variables and their corresponding values.
* @param {Object<string, number|DrawableObject>} values
* @returns {number}
*/
evaluate(values) {
values = Object.assign({}, values, this.parser.consts)
return evaluate(this.tokens, this, values)
}
toString() {
return expressionToString(this.tokens, false)
}
/**
* Returns the list of symbols (string of characters) which are not defined
* as constants or functions.
* @returns {string[]}
*/
variables(options = {}) {
const vars = []
getSymbols(this.tokens, vars, options)
const functions = this.functions
const consts = this.parser.consts
return vars.filter((name) => {
return !(name in functions) && !(name in consts)
})
}
/**
* Converts the expression to a JS function.
* @param {string} param - Parsed variables for the function.
* @param {Object.<string, (ExprEvalExpression|string)>} variables - Default variables to provide.
* @returns {function(...any)}
*/
toJSFunction(param, variables) {
const expr = this
const f = new Function(param, "with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return " + expressionToString(this.simplify(variables).tokens, true) + "; }") // eslint-disable-line no-new-func
return function() {
return f.apply(expr, arguments)
}
}
}