LogarithmPlotter/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/parsing/ast.js

908 lines
29 KiB
JavaScript

/**
* LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
* Copyright (C) 2022 Ad5001
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.pragma library
.import "reference.js" as Reference
.import "../math/latex.js" as Latex
const DERIVATION_PRECISION = 0.01
const ZERO_EPISLON = 5e-11 // Number under which a variable is considered 0 when dealing with floating point rounding errors.
const BINARY_OPERATION_PRIORITY = {
"+": 10, "-": 10,
"*": 20, "/": 20
}
enum ASEType {
UNKNOWN,
VARIABLE,
ARRAY,
PROPERTY,
NUMBER,
STRING,
FUNCTION,
CONSTANT,
UNARY_OPERATION,
BINARY_OPERATION,
TERTIARY_OPERATION,
}
/**
* Base class for abstract syntax elements.
*/
class AbstractSyntaxElement {
type = ASEType.UNKNOWN
/**
* Returns the computed number of value of the element
* depending on the given variables.
*
* @param {Dictionary} variables - variable name/value dictionary representing the variables.
* @throws {EvalError} When the expression is invalid or that variables are missing.
* @returns {number}
*/
execute(variables) {
return null
}
/**
* Simplifies to a maximum the current expression.
*
* @param {array} variables
* @returns {AbstractSyntaxElement}
*/
simplify() {
throw new Error(`Function 'simplify' of ${this.type} not implemented.`)
}
/**
* Substitutes the given variable by another AbstractSyntaxElement.
*
* @param {string} variable
* @param {AbstractSyntaxElement} substitution
* @returns {AbstractSyntaxElement}
*/
substitute(variable, substitution) {
throw new Error(`Function 'substitute' of ${this.type} not implemented.`)
}
/**
* Returns the derivation of this element depending on a variable.
* WARNING: Might -or might not- clone the element.
*
* @param {string} variable
* @returns {AbstractSyntaxElement}
*/
derivation(variable) {
throw new Error(`Function 'derivation' of ${this.type} not implemented.`)
}
/**
* Returns the integral of this element depending on a variable.
* WARNING: Might -or might not- clone the element.
*
* @param {string} variable
* @returns {AbstractSyntaxElement}
*/
integral(variable) {
throw new Error(`Function 'integral' of ${this.type} not implemented.`)
}
/**
* Returns the string that can be reparsed by the parser and be edited by the user.
*
* @returns {string}
*/
toEditableString() {
throw new Error(`Function 'toEditableString' of ${this.type} not implemented.`)
}
/**
* Returns the LaTeX string of this item.
*
* @returns {string}
*/
toLatex() {
throw new Error(`Function 'toLatex' of ${this.type} not implemented.`)
}
/**
* Checks whether the current item is constant or depends on variables.
*
* @returns {bool}
*/
isConstant() {
throw new Error(`Function 'isConstant' of ${this.type} not implemented.`)
}
}
class NumberElement extends AbstractSyntaxElement {
type = ASEType.NUMBER
constructor(number) {
this.value = parseFloat(number)
}
execute(variables) {
return this.value
}
simplify() {
return this
}
subtitute(variable, substitution) {
return this
}
derivation(variable) {
return new NumberElement(0)
}
integral(variable) {
let v = new Variable(variable)
return this.value == 1 ? v : new BinaryOperation(this, '*', v)
}
toEditableString() {
return this.value.toString()
}
toLatex() {
return this.value == Infinity ? "\\infty" : this.value.toString()
}
isConstant() {
return true
}
}
class StringElement extends AbstractSyntaxElement {
type = ASEType.STRING
constructor(str) {
this.str = str
}
execute(variables) {
return this.str
}
simplify() {
return this
}
subtitute(variable, substitution) {
return this
}
derivation(variable) {
return this
}
integral(variable) {
return this
}
toEditableString() {
return '"' + this.str + '"'
}
toLatex() {
return this.str
}
isConstant() {
return true
}
}
class Variable extends AbstractSyntaxElement {
type = ASEType.VARIABLE
constructor(variableName) {
this.variableName = variableName
}
simplify() {
return this
}
subtitute(variable, substitution) {
return variable == this.variableName ? substitution : this
}
execute(variables) {
if(this.variableName in variables)
return variables[this.variableName]
else
throw new EvalError(`Unknown variable ${this.variableName}.`)
}
derivation(variable) {
return new NumberElement(variable == this.variableName ? 1 : 0)
}
integral(variable) {
let op = new BinaryOperation(this, '*', new Variable(variable))
if(variable == this.variableName)
// <var>^2/2
op = new BinaryOperation(new BinaryOperation(this, '^', new NumberElement(2)), '/', new NumberElement(2))
return op
}
toEditableString() {
return this.variableName
}
toLatex() {
return Latex.variable(this.variableName)
}
isConstant() {
return false
}
}
class Constant extends Variable {
type = ASEType.CONSTANT
constructor(constant) {
super(constant)
}
execute(variables) {
if(Reference.CONSTANTS_LIST.includes(this.variableName))
return Reference.CONSTANTS[this.variableName]
else
throw new EvalError(`Unknown constant ${this.variableName}.`)
}
isConstant() {
return true
}
}
class ArrayElement extends AbstractSyntaxElement {
type = ASEType.ARRAY
constructor(astArrayElement, astIndex) {
this.arrayFormula = astArrayElement.toEditableString()
this.astArrayElement = astArrayElement
this.astIndex = astIndex
}
execute(variables) {
let elem = this.astArrayElement.execute(variables)
let index = this.astIndex.execute(variables)
if(Array.isArray(elem)) {
if(index % 1 != 0 || index < 0) { // Float index.
throw new EvalError(`Non-integer array index ${index} used for ${this.arrayFormula}.`)
} else if(elem.length <= index) {
throw new EvalError(`Out-of-range array index ${index} used for ${this.arrayFormula} (has ${elem.length} elements).`)
} else {
return elem[index]
}
} else
throw new EvalError(`${this.arrayFormula} is not an array.`)
}
simplify() {
return new ArrayElement(
this.astArrayElement.simplify(),
this.astIndex.simplify()
)
}
substitute(variable, substitution) {
return new ArrayElement(
this.astArrayElement.substitute(variable, substitution),
this.astIndex.substitute(variable, substitution)
)
}
derivation(variable) {
return new NumberElement(0)
// TODO: Implement derivation depending on value.
}
integral(variable) {
return new BinaryOperation(this,'*',new Variable(variable))
// TODO: Implement integral depending on value.
}
toEditableString() {
return `${this.arrayFormula}[${this.astIndex.toEditableString()}]`
}
toLatex() {
return `${this.astArrayElement.toLatex()}\\left[${this.astIndex.toLatex()}\\right]`
}
isConstant() {
return this.astArrayElement.isConstant() && this.astIndex.isConstant()
}
}
class PropertyElement extends AbstractSyntaxElement {
type = ASEType.PROPERTY
constructor(astObjectElement, propertyName) {
this.astObjectFormula = astObjectElement.toEditableString()
this.astObjectElement = astObjectElement
this.propertyName = propertyName
}
execute(variables) {
let elem = this.astObjectElement.execute(variables)
if(typeof elem == 'object') {
if(this.propertyName in elem)
return elem[propertyName]
else
throw new EvalError(`Property ${propertyName} of ${this.astObjectFormula} does not exist.`)
} else
throw new EvalError(`${this.astObjectFormula} is not an object.`)
}
simplify() {
return new PropertyElement(this.astObjectElement.simplify(), this.propertyName)
}
substitute(variable, substitution) {
return new PropertyElement(
this.astObjectElement.substitute(variable, substitution), this.propertyName
)
}
derivation(variable) {
return new NumberElement(0)
// TODO: Implement derivation depending on value.
}
integral(variable) {
return new BinaryOperation(this,'*',new Variable(variable))
// TODO: Implement integral depending on value.
}
toEditableString() {
return `${this.astObjectFormula}.${this.propertyName}`
}
toLatex() {
return `${this.astObjectFormula.toLatex()}.${this.propertyName}`
}
isConstant() {
return this.astObjectFormula.isConstant()
}
}
/**
* Base class for all functions EXCEPT integral and derivation (see subclasses)
* TODO: Implement function name as elements to have property functions.
**/
class FunctionElement extends AbstractSyntaxElement {
type = ASEType.FUNCTION
constructor(functionName, astArguments) {
this.function = functionName
this.args = astArguments
}
execute(variables) {
if(Reference.FUNCTIONS_LIST.includes(this.function)) {
let args = this.args.map(arg => arg.execute(variables))
return Reference.FUNCTIONS[this.function](...args)
} else
throw new EvalError(`Unknown function ${this.function}.`)
}
simplify() {
let args = this.args.map(arg => arg.simplify(variables))
let newFunc = new FunctionElement(this.function, args)
let result = newFunc
if(newFunc.isConstant() && (result = newFunc.execute({})) % 1 < ZERO_EPISLON)
// Prevent simplification of non constants (e.g. non constant cos, sin...)
newFunc = new NumberElement(result)
return newFunc
}
substitute(variable, substitution) {
}
derivation(variable) {
//TODO: Use DERIVATIVES elements in reference.
return new DerivationElement([this, variable])
}
integral(variable) {
//TODO: Use INTEGRALS elements in reference.
return new IntegralElement([this, variable])
}
toEditableString() {
return `${this.function}(${this.args.map(arg => arg.toEditableString()).join(', ')})`
}
toLatex() {
switch(this.function) {
case "sqrt":
return '\\sqrt{' + this.args.map(arg => arg.toLatex()).join(', ') + '}'
case "abs":
return '\\left|' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right|'
case "floor":
return '\\left\\lfloor' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rfloor'
case "ceil":
return '\\left\\lceil' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right\\rceil'
default:
return '\\mathrm{' + this.function + '}\\left(' + this.args.map(arg => arg.toLatex()).join(', ') + '\\right)'
}
}
isConstant() {
return this.args.every(x => x.isConstant())
}
}
/**
* Signatures supported for derivation:
* - derivation(f,var)
**/
class DerivationElement extends FunctionElement {
constructor(astArguments) {
super("derivation", astArguments)
this.args = astArguments
// Check syntax
if(this.args.length != 2)
throw new Error(`Function 'derivation' can only have 2 arguments. ${this.args.length} provided.`)
if(!(this.args[1] instanceof Variable))
throw new Error(`Argument 1 of function 'derivation' must be a variable.`)
}
execute(variables) {
// Calculate derivation.
// TODO: Do derivation simplification.
let d = this.args[1].variableName // derivation variable name.
if(d in variables) {
let plus = this.args[0].execute(Object.assign(
{[d]: variables[d]+DERIVATION_PRECISION/2}, variables
))
let min = this.args[0].execute(Object.assign(
{[d]: variables[d]-DERIVATION_PRECISION/2}, variables
))
return (plus-min)/DERIVATION_PRECISION
} else
throw new EvalError(`Undefined variable ${d}.`)
}
simplify() {
return new DerivationElement([this.args[0].simplify(variables), this.args[1]])
}
integral(variable) {
// Check if we're integrating and derivating by the same variable
return variable.variableName == this.args[1].variableName ? this.args[1] : super(variable)
}
toLatex() {
return `\\frac{d(${this.args[0].toLatex()})}{d${this.args[1].toLatex()}}`
}
isConstant() {
return this.args[0].isConstant()
}
}
/**
* Signatures supported for integrals:
* - integral(f,var)
* - integral(a,b,f,var)
**/
class IntegralElement extends FunctionElement {
constructor(astArguments) {
super("integral", astArguments)
this.args = astArguments
// Check syntax
if(![2,4].includes(this.args.length))
throw new Error(`Function 'integral' can only have 2 or 4 arguments. ${this.args.length} provided.`)
if(!(this.args[this.args.length-1] instanceof Variable))
// Last argument must always be a variable
throw new Error(`Argument ${this.args.length} of function 'integral' must be a variable.`)
// Setting shortcuts so that we don't have to if every time.
if(this.args.length == 2) {
this.a = new NumberElement(0)
this.b = new Variable('x')
this.f = args[0]
this.d = args[1]
} else {
[this.a, this.b, this.f, this.d] = args
}
}
execute(variables) {
// Calculate integral.
// Using Simpsons rule
// https://en.wikipedia.org/wiki/Simpson%27s_rule
let a = this.a.execute(variables)
let b
try {
b = this.b.execute(variables)
} catch(e) {
if(this.args.length == 2)
throw new EvalError(`Cannot integrate ${this.args[0].toEditableString()}: no limits were defined and x is not a variable.`)
else
throw e
}
let f = this.f.execute
let d = this.d.variableName
// (b-a)/6*(f(a)+4*f((a+b)/2)+f(b))
let f_a = f(Object.assign({[d]: a}, variables))
let f_b = f(Object.assign({[d]: b}, variables))
let f_m = f(Object.assign({[d]: (a+b)/2}, variables))
return (b-a)/6*(f_a+4*f_m+f_b)
}
simplify() {
// TODO: When full derivation and integrals are implemented, use dedicated functions for simplification.
let func = this.args[this.args.length-2].simplify(variables)
let newElem
if(func.isConstant() && this.args.length == 4)
// Simplify integral.
newElem = new BinaryOperation(
new BinaryOperation(this.args[1], '-', this.args[0]).simplify(),
'*',
func
)
else
newElem = new IntegralElement(this.args.length == 4 ?
[this.a.simplify(), this.b.simplify(), func, this.d] :
[func, this.d]
)
return newElem
}
derivation(variable) {
// Check if we're integrating and derivating by the same variable
return variable.variableName == this.args[1].variableName ? this.args[1] : super(variable)
}
toLatex() {
let limits = this.args.length == 2 ? '' :
`\\limits_{${this.b.toLatex()}}^{${this.b.toLatex()}}`
return `\\int${limits}{${this.f.toLatex()}}{d${this.d.toLatex()}}`
}
isConstant() {
return this.a.isConstant() && this.b.isConstant() && this.f.isConstant()
}
}
class BinaryOperation extends AbstractSyntaxElement {
type = ASEType.BINARY_OPERATION
constructor(leftHand, operation, rightHand) {
this.leftHand = leftHand
this.ope = operation
this.rightHand = rightHand
}
evaluate(variables) {
switch(this.ope) {
case '+':
return this.leftHand.evaluate(variables) + this.rightHand.evaluate(variables)
case '-':
return this.leftHand.evaluate(variables) - this.rightHand.evaluate(variables)
case '*':
return this.leftHand.evaluate(variables) * this.rightHand.evaluate(variables)
case '/':
return this.leftHand.evaluate(variables) / this.rightHand.evaluate(variables)
case '%':
return this.leftHand.evaluate(variables) % this.rightHand.evaluate(variables)
case '^':
return Math.pow(this.leftHand.evaluate(variables), this.rightHand.evaluate(variables))
default:
throw new EvalError("Unknown operator " + ope + ".")
}
}
simplify() {
let leftHand = this.leftHand.simplify()
let rightHand = this.rightHand.simplify()
let newOpe = new BinaryOperation(leftHand, this.ope, rightHand)
let result
let tmpResult
if(newOpe.isConstant() && (tmpResult = Math.abs(newOpe.execute({})) < 1000000)) {
// Do not simplify to too big numbers
switch(this.ope) {
case '+':
case '-':
case '*':
case '^':
case '%':
result = new NumberElement(tmpResult)
break
case '/':
if(tmpResult % 1 == 0)
result = new NumberElement(tmpResult)
else {
let simplified = simplifyFraction(leftHand.number, rightHand.number)
result = new BinaryOperation(new NumberElement(simplified[0]), '/', new NumberElement(simplified[1]))
}
break
default:
throw new EvalError("Unknown operator " + ope + ".")
}
} else {
// Simplifications of +- 0 or *1
switch(this.ope) {
case '+':
case '-':
if(leftHand instanceof NumberElement && leftHand.value == 0)
return rightHand
else if(rightHand instanceof NumberElement && rightHand.value == 0) {
if(ope == '-') leftHand.value = -leftHand.value
result = leftHand
} else
result = newOpe
break
case '*':
if((leftHand instanceof NumberElement && leftHand.value == 0) || (rightHand instanceof NumberElement && rightHand.value == 0))
result = new NumberElement(0)
else if(leftHand instanceof NumberElement && leftHand.value == 1)
result = rightHand
else if(rightHand instanceof NumberElement && rightHand.value == 1)
result = leftHand
else
result = newOpe
break
case '^':
if(rightHand instanceof NumberElement && rightHand.value == 0)
result = new NumberElement(1)
else if(rightHand instanceof NumberElement && rightHand.value == 1)
result = new NumberElement(leftHand.value)
else
result = newOpe
break
case '/':
if(rightHand instanceof NumberElement && rightHand.value == 1)
result = new NumberElement(leftHand.value)
else
result = newOpe
break
case '%':
result = newOpe
break
default:
throw new EvalError("Unknown operator " + ope + ".")
}
}
return result
}
substitute(variable, substitution) {
return new BinaryOperation(
this.leftHand.substitute(variable, substitution),
this.ope,
this.rightHand.substitute(variable, substitution)
)
}
derivation(variable) {
switch(this.ope) {
case '-':
case '+':
return new BinaryOperation(this.leftHand.derivation(variable), this.ope, this.rightHand.derivation(variable))
case '*':
// (f*g)' = f'g + fg'
return new BinaryOperation(
new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable)),
'+',
new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand),
)
case '/':
// (f/g)' = (f'g - fg')/g^2
return new BinaryOperation(
new BinaryOperation(
new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable)),
'-',
new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand),
),
'/',
new BinaryOperation(this.rightHand, '^', new NumberElement(2))
)
case '^':
// f^g = e^gln(f) => (e^gln(f))' = (gln(f))'e^gln(f)
// = (gln'(f) + g'ln(f))e^gln(f)
// = (gf'/f + g'ln(f))e^gln(f)
// Bit of a pain to implement, not really worth in terms of 'simplification' for synthesis.
// So I don't use it here.
case '%':
return new DerivationElement([this, new Variable(variable)])
default:
throw new EvalError(`Unknown operator ${ope}.`)
}
}
integral(variable) {
switch(this.ope) {
case '-':
case '+':
return new BinaryOperation(this.leftHand.integral(variable), this.ope, this.rightHand.integral(variable))
case '*':
return new BinaryOperation(
new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand),
'+',
new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable))
)
case '/':
return new BinaryOperation(
new BinaryOperation(this.leftHand.derivation(variable), '*', this.rightHand),
'+',
new BinaryOperation(this.leftHand, '*', this.rightHand.derivation(variable))
)
case '^':
case '%':
return new IntegralElement("integral", this.toEditableString())
default:
throw new EvalError(`Unknown operator ${ope}.`)
}
}
toEditableString() {
let leftString = this.leftHand.toEditableString()
let rightString = this.rightHand.toEditableString()
if(this.leftHand.type == ASEType.BINARY_OPERATION &&
BINARY_OPERATION_PRIORITY[this.ope] > BINARY_OPERATION_PRIORITY[this.leftHand.ope])
leftString = "(" + leftString + ")"
if(this.rightHand.type == ASEType.BINARY_OPERATION &&
BINARY_OPERATION_PRIORITY[this.ope] > BINARY_OPERATION_PRIORITY[this.rightHand.ope])
rightString = "(" + rightString + ")"
return `${leftString} ${this.ope} ${rightString}`
}
toLatex() {
switch(this.ope) {
case '-':
case '+':
return this.leftHand.toLatex() + this.ope + this.rightHand.toLatex()
case '*':
return this.leftHand.toLatex() + " \\times " + this.rightHand.toLatex()
case '%':
return this.leftHand.toLatex() + " \\mathrm{mod} " + this.rightHand.toLatex()
case '/':
return "\\frac{" + this.leftHand.toLatex() + "}{" + this.rightHand.toLatex() + "}"
case '^':
return this.leftHand.toLatex() + "^{" + this.rightHand.toLatex() + "}"
default:
throw new EvalError("Unknown operator " + ope + ".")
}
return this.leftHand.toLatex() + ope + this.rightHand.toLatex()
}
isConstant() {
return this.leftHand.isConstant() && this.rightHand.isConstant()
}
}
function simplifyFraction(num,den) {
// More than gcd because it allows decimals fractions.
let mult = 1
if(num%1 != 0)
mult = Math.max(mult,Math.pow(10,num.toString().split('.')[1].length))
else if(den%1 != 0)
mult = Math.max(mult,Math.pow(10,den.toString().split('.')[1].length))
let a = Math.abs(num*mult)
let b = Math.abs(den*mult)
let gcd = 0
if (b > a) {let temp = a; a = b; b = temp;}
while (gcd == 0) {
if (b == 0) gcd = a
a %= b
if (a == 0) gcd = b
b %= a
}
return [num*mult/gcd, den*mult/gcd]
}
class Negation extends AbstractSyntaxElement {
type = ASEType.NEGATION
constructor(variableName) {
this.variableName = variableName
}
execute(variables) {
if(this.variableName in variables) {
return variables[this.variableName]
} else {
throw new EvalError("Unknown variable " + this.variableName + ".")
}
}
derivation(variable) {
if(variable == this.variableName)
return new NumberElement(1)
return this
}
integral(variable) {
if(variable == this.variableName)
// <var>^2/2
return new BinaryOperation(new BinaryOperation(this, '^', new NumberElement(2)), '/', new NumberElement(2))
return this
}
toEditableString() {
return this.variableName
}
toLatex() {
return this.variableName
}
isConstant() {
return false
}
}
class Negation extends AbstractSyntaxElement {
type = ASEType.NEGATION
constructor(expression) {
this.expression = expression
}
execute(variables) {
if(variables.includes(this.arrayName)) {
let index = this.astIndex.execute(variables)
if(index % 1 != 0 || index < 0) { // Float index.
throw new EvalError("Non-integer array index " + index + " used as array index for " + this.variableName + ".")
} else if(variables[this.arrayName].length <= index) {
throw new EvalError("Out-of-range index " + index + " used as array index for " + this.variableName + ".")
} else {
return variables[this.arrayName][index]
}
} else {
throw new EvalError("Unknown variable " + this.variableName + ".")
}
toLatex() {
return this.variableName
}
}
simplify() {
return new Negation(this.expression.simplify())
}
derivation(variable) {
return new Negation(this.expression.derivation(variable))
}
integral(variable) {
return new Negation(this.expression.integral(variable))
}
toLatex() {
return '-' + this.expression.toLatex()
}
isConstant() {
return this.expression.isConstant()
}
}