mathjslab
Version:
MathJSLab - An interpreter with language syntax like MATLAB®/Octave. ISBN 978-65-00-82338-7
1,316 lines (1,246 loc) • 74.1 kB
text/typescript
/**
* MATLAB®/Octave like syntax parser/interpreter/compiler.
*/
import { constantsTable } from './constants-table';
import { substSymbol } from './subst-symbol';
import { CharString } from './char-string';
import { ComplexDecimal } from './complex-decimal';
import { MultiArray } from './multi-array';
import { CoreFunctions } from './core-functions';
import { LinearAlgebra } from './linear-algebra';
import { MathObject } from './math-object';
import Parser from './parser.js';
/**
* Operator type.
*/
export type TOperator =
| '+'
| '-'
| '.*'
| '*'
| './'
| '/'
| '.\\'
| '\\'
| '.^'
| '^'
| '.**'
| '**'
| '<'
| '<='
| '=='
| '>='
| '>'
| '!='
| '~='
| '&'
| '|'
| '&&'
| '||'
| '='
| '+='
| '-='
| '*='
| '/='
| '\\='
| '^='
| '**='
| '.*='
| './='
| '.\\='
| '.^='
| '.**='
| '&='
| '|='
| '()'
| '!'
| '~'
| '+_'
| '-_'
| '++_'
| '--_'
| ".'"
| "'"
| '_++'
| '_--';
/**
* aliasNameTable type.
*/
export type TAliasNameTable = Record<string, RegExp>;
/**
* baseFunctionTable type.
*/
export type TBaseFunctionTableEntry = {
mapper?: boolean;
ev: boolean[];
func: Function;
unparserMathML?: (tree: any) => string;
};
export type TBaseFunctionTable = Record<string, TBaseFunctionTableEntry>;
/**
* nameTable type.
*/
export type TNameTableEntry = {
args: Array<any>;
expr: any;
};
export type TNameTable = Record<string, TNameTableEntry>;
/**
* commandWordListTable type.
*/
export type TCommandWordListFunction = (...args: string[]) => void;
export type TCommandWordListTableEntry = {
func: TCommandWordListFunction;
};
export type TCommandWordListTable = Record<string, TCommandWordListTableEntry>;
/**
* TEvaluatorConfig type.
*/
export type TEvaluatorConfig = {
aliasTable?: TAliasNameTable;
externalFunctionTable?: TBaseFunctionTable;
externalCmdWListTable?: TCommandWordListTable;
};
/**
* AST (Abstract Syntax Tree) nodes.
*/
/**
* Common primary node.
*/
interface PrimaryNode {
type: string | number;
parent?: any;
index?: number;
}
/**
* Expression node.
*/
export type NodeExpr = NodeName | NodeArgExpr | NodeOperation | NodeList | NodeRange | NodeReturnList | MultiArray | ComplexDecimal;
/**
* Reserved node.
*/
interface NodeReserved extends PrimaryNode {}
/**
* Name node.
*/
export interface NodeName extends PrimaryNode {
type: 'NAME';
id: string;
}
/**
* Command word list node.
*/
interface NodeCmdWList extends PrimaryNode {
type: 'CmdWList';
id: string;
args: Array<CharString>;
}
/**
* Expression and arguments node.
*/
export interface NodeArgExpr extends PrimaryNode {
type: 'ARG';
expr: NodeExpr;
args: Array<NodeExpr>;
}
/**
* Range node.
*/
interface NodeRange extends PrimaryNode {
type: 'RANGE';
start: NodeExpr | null;
stop: NodeExpr | null;
stride: NodeExpr | null;
}
/**
* Operation node.
*/
export type NodeOperation = UnaryOperation | BinaryOperation;
/**
* Unary operation node.
*/
type UnaryOperation = UnaryOperationL | UnaryOperationR;
/**
* Right unary operation node.
*/
interface UnaryOperationR extends PrimaryNode {
right: NodeExpr;
}
/**
* Left unary operation node.
*/
interface UnaryOperationL extends PrimaryNode {
left: NodeExpr;
}
/**
* Binary operation.
*/
interface BinaryOperation extends PrimaryNode {
left: NodeExpr;
right: NodeExpr;
}
/**
* List node
*/
export interface NodeList extends PrimaryNode {
type: 'LIST';
list: Array<NodeExpr>;
}
export type ReturnSelector = (length: number, index: number) => any;
/**
* Return list node
*/
export interface NodeReturnList extends PrimaryNode {
type: 'RETLIST';
selector: ReturnSelector;
}
/**
* External parser declarations (defined in parser body)
*/
declare global {
/* eslint-disable-next-line no-var */
var EvaluatorPointer: Evaluator;
/* eslint-disable-next-line no-var */
var commandsTable: string[];
}
/**
* Evaluator object.
* It is implemented as a class but cannot be instantiated more than one time
* simultaneously. Instance is given by `Evaluator.initialize` static method
* or global variable global.EvaluatorPointer.
*/
export class Evaluator {
/**
* After run Parser or Evaluate method, the exitStatus property will contains
* exit state of method.
*/
public static response = {
EXTERNAL: -2,
WARNING: -1,
OK: 0,
LEX_ERROR: 1,
PARSER_ERROR: 2,
EVAL_ERROR: 3,
};
/**
* Debug flag, setter and getter.
*/
private _debug: boolean = false;
public get debug(): boolean {
return this._debug;
}
public set debug(value: boolean) {
this._debug = value;
}
/**
* Native name table. It's inserted in nameTable when
* `Evaluator.initialize` executed.
*/
private readonly nativeNameTable: Record<string, ComplexDecimal> = {
false: ComplexDecimal.false(),
true: ComplexDecimal.true(),
i: ComplexDecimal.onei(),
I: ComplexDecimal.onei(),
j: ComplexDecimal.onei(),
J: ComplexDecimal.onei(),
e: ComplexDecimal.e(),
pi: ComplexDecimal.pi(),
inf: ComplexDecimal.inf_0(),
Inf: ComplexDecimal.inf_0(),
nan: ComplexDecimal.NaN_0(),
NaN: ComplexDecimal.NaN_0(),
};
/**
* Name table.
*/
public nameTable: TNameTable = {};
public readonlyNameTable: string[] = [];
/**
* Alias table.
*/
private aliasTable: TAliasNameTable;
/**
* Base function table.
*/
public baseFunctionTable: TBaseFunctionTable = {};
/**
* Get a list of names of defined functions in baseFunctionTable.
*/
public get baseFunctionList(): string[] {
return Object.keys(this.baseFunctionTable);
}
/**
* Local table.
*/
public localTable: Record<string, any> = {};
/**
* Command word list table.
*/
public commandWordListTable: TCommandWordListTable = {};
/**
* Parser (generated by Jison).
*/
private readonly parser: { parse: (input: string) => any } = Parser;
/**
* Evaluator exit status.
*/
public exitStatus: number;
/**
* Increment and decrement operator
* @param pre `true` if prefixed. `false` if postfixed.
* @param operation Operation (`'plus'` or `'minus'`).
* @returns Operator function with signature `(tree: any) => any`.
*/
private incDecOp(pre: boolean, operation: 'plus' | 'minus'): (tree: any) => any {
if (pre) {
return (tree: any): any => {
if (tree.type === 'NAME') {
if (this.nameTable[tree.id].expr) {
this.nameTable[tree.id].expr = MathObject[operation](this.nameTable[tree.id].expr, ComplexDecimal.one());
return this.nameTable[tree.id].expr;
} else {
throw new EvalError('in x++ or ++x, x must be defined first.');
}
} else {
throw new SyntaxError(`invalid ${operation === 'plus' ? 'increment' : 'decrement'} variable.`);
}
};
} else {
return (tree: any): any => {
if (tree.type === 'NAME') {
if (this.nameTable[tree.id].expr) {
const value = MathObject.copy(this.nameTable[tree.id].expr);
this.nameTable[tree.id].expr = MathObject[operation](this.nameTable[tree.id].expr, ComplexDecimal.one());
return value;
} else {
throw new EvalError('in x++ or ++x, x must be defined first.');
}
} else {
throw new SyntaxError(`invalid ${operation === 'plus' ? 'increment' : 'decrement'} variable.`);
}
};
}
}
/**
* Operator table.
*/
private readonly opTable: Record<string, Function> = {
'+': MathObject.plus,
'-': MathObject.minus,
'.*': MathObject.times,
'*': MathObject.mtimes,
'./': MathObject.rdivide,
'/': MathObject.mrdivide,
'.\\': MathObject.ldivide,
'\\': MathObject.mldivide,
'.^': MathObject.power,
'^': MathObject.mpower,
'+_': MathObject.uplus,
'-_': MathObject.uminus,
".'": MathObject.transpose,
"'": MathObject.ctranspose,
'<': MathObject.lt,
'<=': MathObject.le,
'==': MathObject.eq,
'>=': MathObject.ge,
'>': MathObject.gt,
'!=': MathObject.ne,
'&': MathObject.and,
'|': MathObject.or,
'!': MathObject.not,
'&&': MathObject.mand,
'||': MathObject.mor,
'++_': this.incDecOp(true, 'plus'),
'--_': this.incDecOp(true, 'minus'),
'_++': this.incDecOp(false, 'plus'),
'_--': this.incDecOp(false, 'minus'),
};
/**
* User functions.
*/
private readonly functions: Record<string, Function> = {
unparse: (tree: any): CharString => new CharString(EvaluatorPointer.Unparse(tree)),
};
/**
* Parser AST (Abstract Syntax Tree) constructor methods.
*/
public readonly nodeString = CharString.parse;
public readonly isString = CharString.isThis;
public readonly unparseString = CharString.unparse;
public readonly unparseStringMathML = CharString.unparseMathML;
public readonly removeQuotes = CharString.removeQuotes;
public readonly nodeNumber = ComplexDecimal.parse;
public readonly newNumber = ComplexDecimal.newThis;
public readonly isNumber = ComplexDecimal.isThis;
public readonly unparseNumber = ComplexDecimal.unparse;
public readonly unparseNumberMathML = ComplexDecimal.unparseMathML;
public readonly isTensor = MultiArray.isThis;
public readonly isRowVector = MultiArray.isRowVector;
public readonly unparseTensor = MultiArray.unparse;
public readonly unparseTensorMathML = MultiArray.unparseMathML;
public readonly evaluateTensor = MultiArray.evaluate;
public readonly mapTensor = MultiArray.map;
public readonly getElements = MultiArray.getElements;
public readonly getElementsLogical = MultiArray.getElementsLogical;
public readonly setElements = MultiArray.setElements;
public readonly setElementsLogical = MultiArray.setElementsLogical;
public readonly expandRange = MultiArray.expandRange;
public readonly firstRow = MultiArray.firstRow;
public readonly appendRow = MultiArray.appendRow;
public readonly tensor0x0 = MultiArray.array_0x0;
public readonly linearize = MultiArray.linearize;
public readonly toTensor = MultiArray.scalarToMultiArray;
public readonly linearLength = MultiArray.linearLength;
public readonly getDimension = MultiArray.getDimension;
/**
* Special functions MathML unparser.
*/
private readonly unparseMathMLFunctions: Record<string, (tree: any) => string> = {
abs: (tree: any) => '<mrow><mo>|</mo>' + this.unparserMathML(tree.args[0]) + '<mo>|</mo></mrow>',
conj: (tree: any) => '<mover><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow><mo>‾</mo></mover>',
sqrt: (tree: any) => '<msqrt><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow></msqrt>',
root: (tree: any) => '<mroot><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow><mrow>' + this.unparserMathML(tree.args[1]) + '</mrow></mroot>',
exp: (tree: any) => '<msup><mi>e</mi><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow></msup>',
logb: (tree: any) => '<msub><mi>log</mi><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow></msub><mrow>' + this.unparserMathML(tree.args[1]) + '</mrow>',
log2: (tree: any) => '<msub><mi>log</mi><mrow>' + '<mn>2</mn>' + '</mrow></msub><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow>',
log10: (tree: any) => '<msub><mi>log</mi><mrow>' + '<mn>10</mn>' + '</mrow></msub><mrow>' + this.unparserMathML(tree.args[0]) + '</mrow>',
gamma: (tree: any) => '<mi>Γ</mi><mrow><mo>(</mo>' + this.unparserMathML(tree.args[0]) + '<mo>)</mo></mrow>',
factorial: (tree: any) => '<mrow><mo>(</mo>' + this.unparserMathML(tree.args[0]) + '<mo>)</mo></mrow><mo>!</mo>',
};
/**
* Evaluator object constructor
*/
private constructor() {
global.EvaluatorPointer = this;
this.exitStatus = Evaluator.response.OK;
/* Set opTable aliases */
this.opTable['**'] = this.opTable['^'];
this.opTable['.**'] = this.opTable['.^'];
this.opTable['~='] = this.opTable['!='];
this.opTable['~'] = this.opTable['!'];
/* Load nativeNameTable and constantsTable in nameTable */
this.loadNativeTable();
/* Define Evaluator functions */
for (const func in this.functions) {
this.defineFunction(func, this.functions[func]);
}
/* Define function operators */
for (const func in MathObject.twoMoreOpFunction) {
this.defineTwoOrMoreOperandFunction(func, MathObject.twoMoreOpFunction[func]);
}
for (const func in MathObject.binaryOpFunction) {
this.defineBinaryOperatorFunction(func, MathObject.binaryOpFunction[func]);
}
for (const func in MathObject.unaryOpFunction) {
this.defineUnaryOperatorFunction(func, MathObject.unaryOpFunction[func]);
}
/* Define function mappers */
for (const func in ComplexDecimal.mapFunction) {
this.defineFunction(func, ComplexDecimal.mapFunction[func], true);
}
/* Define other functions */
for (const func in ComplexDecimal.twoArgFunction) {
this.defineFunction(func, ComplexDecimal.twoArgFunction[func]);
}
/* Define CoreFunctions functions */
for (const func in CoreFunctions.functions) {
this.defineFunction(func, CoreFunctions.functions[func]);
}
/* Define LinearAlgebra functions */
for (const func in LinearAlgebra.functions) {
this.defineFunction(func, LinearAlgebra.functions[func]);
}
/* Load unparserMathML for special functions */
for (const func in this.unparseMathMLFunctions) {
this.baseFunctionTable[func].unparserMathML = this.unparseMathMLFunctions[func];
}
}
/**
* Evaluator initialization.
* @param config Evaluator configuration.
* @returns Evaluator instance.
*/
public static initialize(config?: TEvaluatorConfig): Evaluator {
const evaluator = new Evaluator();
if (config) {
if (config.aliasTable) {
evaluator.aliasTable = config.aliasTable;
evaluator.aliasName = (name: string): string => {
let result = false;
let aliasname = '';
for (const i in evaluator.aliasTable) {
if (evaluator.aliasTable[i].test(name)) {
result = true;
aliasname = i;
break;
}
}
if (result) {
return aliasname;
} else {
return name;
}
};
} else {
evaluator.aliasName = (name: string): string => name;
}
if (config.externalFunctionTable) {
Object.assign(evaluator.baseFunctionTable, config.externalFunctionTable);
}
if (config.externalCmdWListTable) {
Object.assign(evaluator.commandWordListTable, config.externalCmdWListTable);
}
for (const cmd in evaluator.commandWordListTable) {
global.commandsTable.push(cmd);
}
} else {
evaluator.aliasName = (name: string): string => name;
}
return evaluator;
}
/**
* Alias name function. This property is set when running Evaluator.initialize.
* @param name Alias name.
* @returns Canonical name.
*/
public aliasName: (name: string) => string = (name: string): string => name;
/**
* Load native name table in name table.
*/
private loadNativeTable(): void {
/* Insert nativeNameTable in nameTable */
for (const name in this.nativeNameTable) {
this.nameTable[name] = { args: [], expr: this.nativeNameTable[name] };
this.readonlyNameTable.push(name);
}
/* Insert constantsTable in nameTable */
for (const name in constantsTable) {
this.nameTable[constantsTable[name][0]] = { args: [], expr: constantsTable[name][1] };
this.readonlyNameTable.push(constantsTable[name][0]);
}
}
/**
* Restart evaluator.
*/
public Restart(): void {
this.nameTable = {};
this.localTable = {};
this.readonlyNameTable = [];
this.loadNativeTable();
}
/**
* Clear variables. If names is 0 lenght restart evaluator.
* @param names Variable names to clear in nameTable and basFunctionTable.
*/
public Clear(...names: string[]): void {
if (names.length === 0) {
this.Restart();
} else {
names.forEach((name) => {
if (!this.readonlyNameTable.includes(name)) {
delete this.nameTable[name];
delete this.baseFunctionTable[name];
}
});
}
}
/**
* Parse input.
* @param input Input string
* @returns Parsed input.
*/
public Parse(input: string): any {
return this.parser.parse(input);
}
/**
* Create reserved node.
* @param nodeid
* @returns
*/
public nodeReserved(nodeid: string): NodeReserved {
return { type: nodeid };
}
/**
* Create name node.
* @param nodeid
* @returns
*/
public nodeName(nodeid: string): NodeName {
return {
type: 'NAME',
id: nodeid.replace(/(\r\n|[\n\r])|[\ ]/gm, ''),
};
}
/**
* Create command word list node.
* @param nodename
* @param nodelist
* @returns
*/
public nodeCmdWList(nodename: NodeName, nodelist: NodeList): NodeCmdWList {
return {
type: 'CmdWList',
id: nodename.id,
args: nodelist ? (nodelist.list as any) : [],
};
}
/**
* Create expression and arguments node.
* @param nodeexpr
* @param nodelist
* @returns
*/
public nodeArgExpr(nodeexpr: any, nodelist?: any): NodeArgExpr {
return {
type: 'ARG',
expr: nodeexpr,
args: nodelist ? nodelist.list : [],
};
}
/**
* Create range node. If two arguments are passed then it is 'start' and
* 'stop' of range. If three arguments are passed then it is 'start',
* 'stride' and 'stop'.
* @param args 'start' and 'stop' or 'start', 'stride' and 'stop'.
* @returns NodeRange.
*/
public nodeRange(...args: any): NodeRange {
if (args.length === 2) {
return {
type: 'RANGE',
start: args[0],
stop: args[1],
stride: null,
};
} else if (args.length === 3) {
return {
type: 'RANGE',
start: args[0],
stop: args[2],
stride: args[1],
};
} else {
throw new SyntaxError('invalid range.');
}
}
/**
* Create operator node.
* @param op
* @param data1
* @param data2
* @returns
*/
public nodeOp(op: TOperator, data1: any, data2?: any): NodeOperation {
switch (op) {
case '+':
case '-':
case '.*':
case '*':
case './':
case '/':
case '.\\':
case '\\':
case '.^':
case '^':
case '.**':
case '**':
case '<':
case '<=':
case '==':
case '>=':
case '>':
case '!=':
case '~=':
case '&':
case '|':
case '&&':
case '||':
case '=':
case '+=':
case '-=':
case '*=':
case '/=':
case '\\=':
case '^=':
case '**=':
case '.*=':
case './=':
case '.\\=':
case '.^=':
case '.**=':
case '&=':
case '|=':
return { type: op, left: data1, right: data2 };
case '()':
case '!':
case '~':
case '+_':
case '-_':
case '++_':
case '--_':
return { type: op, right: data1 };
case ".'":
case "'":
case '_++':
case '_--':
return { type: op, left: data1 };
default:
return { type: 'INVALID' } as NodeOperation;
}
}
/**
* Create first element of list node.
* @param node First element of list node.
* @returns A NodeList.
*/
public nodeListFirst(node?: any): NodeList {
if (node) {
const result = {
type: 'LIST',
list: [node],
};
node.parent = result;
return result as NodeList;
} else {
return {
type: 'LIST',
list: [],
};
}
}
/**
* Append node to list node.
* @param lnode NodeList.
* @param node Element to append to list.
* @returns NodeList with element appended.
*/
public nodeList(lnode: NodeList, node: any): NodeList {
node.parent = lnode;
lnode.list.push(node);
return lnode;
}
/**
* Create first row of a MultiArray.
* @param row
* @returns
*/
public nodeFirstRow(row: NodeList): MultiArray {
if (row) {
return this.firstRow(row.list);
} else {
return this.tensor0x0();
}
}
/**
* Append row to MultiArray.
* @param M
* @param row
* @returns
*/
public nodeAppendRow(M: MultiArray, row: NodeList): MultiArray {
if (row) {
return this.appendRow(M, row.list);
} else {
return M;
}
}
/**
* Creates NodeReturnList (multiple assignment)
* @param selector Left side selector function.
* @returns Return list node.
*/
public nodeReturnList(selector: ReturnSelector): NodeReturnList {
return {
type: 'RETLIST',
selector,
};
}
/**
* Throws error if left hand side length of multiple assignment greater
* than maximum length (to be used in ReturnSelector functions).
* @param maxLength Maximum length of return list.
* @param currentLength Requested length of return list.
*/
public throwErrorIfGreaterThanReturnList(maxLength: number, currentLength: number): void {
if (currentLength > maxLength) {
throw new EvalError(`element number ${maxLength + 1} undefined in return list`);
}
}
/**
* Tests if it is a NodeReturnList and if so reduces it to its first
* element.
* @param value A node.
* @returns Reduced node if `tree` is a NodeReturnList.
*/
public reduceIfReturnList(tree: any): any {
if (tree.type === 'RETLIST') {
const result = tree.selector(1, 0);
result.parent = tree.parent;
return result;
} else {
return tree;
}
}
/**
* Validate left side of assignment node.
* @param tree Left side of assignment node
* @param shallow True if tree is a left root of assignment.
* @returns An object with three properties: `left`, `id` and `args`.
*/
public validateAssignment(tree: any, shallow: boolean = true): { left: any; id: string; args: any[] }[] {
const invalidMessageBase = 'invalid left hand side of assignment';
const invalidMessage = `${invalidMessageBase}: cannot assign to a read only value:`;
if (tree.type === 'NAME') {
if (this.readonlyNameTable.includes(tree.id)) {
throw new EvalError(`${invalidMessage} ${tree.id}.`);
}
return [
{
left: tree,
id: tree.id,
args: [],
},
];
} else if (tree.type === 'ARG' && tree.expr.type === 'NAME') {
if (this.readonlyNameTable.includes(tree.expr.id)) {
throw new EvalError(`${invalidMessage} ${tree.expr.id}.`);
}
return [
{
left: tree.expr,
id: tree.expr.id,
args: tree.args,
},
];
} else if (tree.type === '<~>') {
return [
{
left: null,
id: '~',
args: [],
},
];
} else if (shallow && this.isRowVector(tree)) {
return tree.array[0].map((left: any) => this.validateAssignment(left, false)[0]);
} else {
throw new EvalError(`${invalidMessageBase}.`);
}
}
/**
* Define function in baseFunctionTable.
* @param name Name of function.
* @param func Function body.
* @param map `true` if function is a mapper function.
* @param ev A `boolean` array indicating which function argument should
* be evaluated before executing the function.
*/
private defineFunction(name: string, func: Function, map?: boolean, ev?: boolean[]): void {
this.baseFunctionTable[name] = {
mapper: map ?? false,
ev: ev ?? [],
func,
};
}
/**
* Define unary operator function in baseFunctionTable.
* @param name Name of function.
* @param func Function body.
*/
private defineUnaryOperatorFunction(name: string, func: Function): void {
this.baseFunctionTable[name] = {
mapper: false,
ev: [],
func: (...operand: any) => {
if (operand.length === 1) {
return func(operand[0]);
} else {
throw new EvalError(`Invalid call to ${name}.`);
}
},
};
}
/**
* Define binary operator function in baseFunctionTable.
* @param name Name of function.
* @param func Function body.
*/
private defineBinaryOperatorFunction(name: string, func: Function): void {
this.baseFunctionTable[name] = {
mapper: false,
ev: [],
func: (left: any, ...right: any) => {
if (right.length === 1) {
return func(left, right[0]);
} else {
throw new EvalError(`Invalid call to ${name}.`);
}
},
};
}
/**
* Define define two-or-more operand function in baseFunctionTable.
* @param name
* @param func
*/
private defineTwoOrMoreOperandFunction(name: string, func: Function): void {
this.baseFunctionTable[name] = {
mapper: false,
ev: [],
func: (left: any, ...right: any) => {
if (right.length === 1) {
return func(left, right[0]);
} else if (right.length > 1) {
let result = func(left, right[0]);
for (let i = 1; i < right.length; i++) {
result = func(result, right[i]);
}
return result;
} else {
throw new EvalError(`Invalid call to ${name}.`);
}
},
};
}
/**
* Expression tree recursive evaluator.
* @param tree Expression to evaluate.
* @param local Set `true` if evaluating function.
* @param fname Function name if evaluating function.
* @returns Expression `tree` evaluated.
*/
public Evaluator(tree: any, local: boolean = false, fname: string = ''): any {
if (this._debug) {
console.log(
`Evaluator(\ntree:${JSON.stringify(
tree,
(key: string, value: any) => (key !== 'parent' ? value : value === null ? 'root' : true),
2,
)},\nlocal:${local},\nfname:${fname});`,
);
}
if (this.isNumber(tree) || this.isString(tree)) {
/* NUMBER or STRING */
return tree;
} else if (this.isTensor(tree)) {
/* MATRIX */
return this.evaluateTensor(tree, local, fname);
} else {
switch (tree.type) {
case '+':
case '-':
case '.*':
case '*':
case './':
case '/':
case '.\\':
case '\\':
case '.^':
case '^':
case '.**':
case '**':
case '<':
case '<=':
case '==':
case '>=':
case '>':
case '!=':
case '~=':
case '&':
case '|':
case '&&':
case '||':
tree.left.parent = tree;
tree.right.parent = tree;
return this.opTable[tree.type](
this.reduceIfReturnList(this.Evaluator(tree.left, local, fname)),
this.reduceIfReturnList(this.Evaluator(tree.right, local, fname)),
);
case '()':
tree.right.parent = tree;
return this.reduceIfReturnList(this.Evaluator(tree.right, local, fname));
case '+_':
case '-_':
tree.right.parent = tree;
return this.opTable[tree.type](this.reduceIfReturnList(this.Evaluator(tree.right, local, fname)));
case '++_':
case '--_':
tree.right.parent = tree;
return this.opTable[tree.type](tree.right);
case ".'":
case "'":
tree.left.parent = tree;
return this.opTable[tree.type](this.reduceIfReturnList(this.Evaluator(tree.left, local, fname)));
case '_++':
case '_--':
tree.left.parent = tree;
return this.opTable[tree.type](tree.left);
case '=':
case '+=':
case '-=':
case '*=':
case '/=':
case '\\=':
case '^=':
case '**=':
case '.*=':
case './=':
case '.\\=':
case '.^=':
case '.**=':
case '&=':
case '|=':
tree.left.parent = tree;
tree.right.parent = tree;
const assignment = this.validateAssignment(tree.left);
const op: TOperator = tree.type.substring(0, tree.type.length - 1);
if (assignment.length > 1 && op.length > 0) {
throw new EvalError('computed multiple assignment not allowed.');
}
let right: any;
try {
right = this.Evaluator(tree.right, false, fname);
} catch {
right = tree.right;
}
if (right.type !== 'RETLIST') {
right = this.nodeReturnList((length: number, index: number) => {
if (index === 0) {
return tree.right;
} else {
throw new EvalError(`element number ${index + 1} undefined in return list`);
}
});
}
const resultList = this.nodeListFirst();
for (let n = 0; n < assignment.length; n++) {
const { left, id, args } = assignment[n];
if (left) {
if (args.length === 0) {
/* Name definition. */
if (right.type !== 'RETLIST') {
right = this.Evaluator(right, false, fname);
}
const rightN = right.selector(assignment.length, n);
rightN.parent = tree.right;
const expr = op.length ? this.nodeOp(op, left, rightN) : rightN;
try {
this.nameTable[id] = { args: [], expr: this.reduceIfReturnList(this.Evaluator(expr)) };
this.nodeList(resultList, this.nodeOp('=', left, this.nameTable[id].expr));
continue;
} catch (error) {
this.nameTable[id] = { args: [], expr: expr };
throw error;
}
} else {
/* Function definition or indexed matrix reference. */
if (op) {
if (typeof this.nameTable[id] !== 'undefined') {
if (this.nameTable[id].args.length === 0) {
/* Indexed matrix reference on left hand side with operator. */
if (args.length === 1) {
/* Test logical indexing. */
const arg0 = this.reduceIfReturnList(this.Evaluator(args[0], local, fname));
if (this.isTensor(arg0) && arg0.type === ComplexDecimal.numberClass.logical) {
/* Logical indexing. */
this.setElementsLogical(
this.nameTable,
id,
this.linearize(arg0),
this.toTensor(
this.reduceIfReturnList(
this.Evaluator(
this.nodeOp(
op,
this.getElementsLogical(this.nameTable[id].expr, id, arg0),
this.toTensor(this.reduceIfReturnList(this.Evaluator(right.selector(assignment.length, n)))),
),
false,
fname,
),
),
),
);
} else {
/* Not logical indexing. */
this.setElements(
this.nameTable,
id,
[arg0],
this.toTensor(
this.reduceIfReturnList(
this.Evaluator(
this.nodeOp(
op,
this.getElements(this.nameTable[id].expr, id, [arg0]),
this.toTensor(this.reduceIfReturnList(this.Evaluator(right.selector(assignment.length, n)))),
),
false,
fname,
),
),
),
);
}
} else {
this.setElements(
this.nameTable,
id,
args.map((arg: any) => this.reduceIfReturnList(this.Evaluator(arg))),
this.toTensor(
this.reduceIfReturnList(
this.Evaluator(
this.nodeOp(
op,
this.getElements(this.nameTable[id].expr, id, args),
this.toTensor(this.reduceIfReturnList(this.Evaluator(right.selector(assignment.length, n)))),
),
false,
fname,
),
),
),
);
}
this.nodeList(resultList, this.nodeOp('=', this.nodeName(id), this.nameTable[id].expr));
continue;
} else {
throw new EvalError(`in computed assignment ${id}(index) OP= X, ${id} cannot be a function.`);
}
} else {
throw new EvalError(`in computed assignment ${id}(index) OP= X, ${id} must be defined first.`);
}
} else {
/* Test if is a function definition (test if args is a list of undefined NAME). */
let isFunction: boolean = true;
for (let i = 0; i < args.length; i++) {
isFunction &&= args[i].type === 'NAME';
if (isFunction) {
isFunction &&= typeof this.nameTable[args[i].id] === 'undefined';
}
if (!isFunction) {
break;
}
}
if (isFunction) {
this.nameTable[id] = { args: args, expr: right.selector(assignment.length, n) };
this.nodeList(resultList, tree);
continue;
} else {
/* Indexed matrix reference on left hand side. */
if (args.length === 1) {
/* Test logical indexing. */
args[0].parent = tree.left;
args[0].index = 0;
const arg0 = this.reduceIfReturnList(this.Evaluator(args[0], local, fname));
if (this.isTensor(arg0) && arg0.type === ComplexDecimal.numberClass.logical) {
/* Logical indexing. */
this.setElementsLogical(
this.nameTable,
id,
this.linearize(arg0),
this.toTensor(this.reduceIfReturnList(this.Evaluator(right.selector(assignment.length, n)))),
);
} else {
/* Not logical indexing. */
this.setElements(
this.nameTable,
id,
[arg0],
this.toTensor(this.reduceIfReturnList(this.Evaluator(right.selector(assignment.length, n)))),
);
}
} else {
this.setElements(
this.nameTable,
id,
args.map((arg: any, i: number) => {
arg.parent = tree.left;
arg.index = i;
return this.reduceIfReturnList(this.Evaluator(arg));
}),
this.toTensor(this.reduceIfReturnList(this.Evaluator(right.selector(assignment.length, n)))),
);
}
this.nodeList(resultList, this.nodeOp('=', this.nodeName(id), this.nameTable[id].expr));
continue;
}
}
}
}
}
if (tree.parent.parent === null) {
/* assignment at root expression */
if (resultList.list.length === 1) {
/* single assignment */
return resultList.list[0];
} else {
/* multiple assignment */
return resultList;
}
} else {
/* assignment at right side */
return (resultList.list[0] as any).right;
}
case 'NAME':
if (local && this.localTable[fname] && this.localTable[fname][tree.id]) {
/* Defined in localTable. */
this.localTable[fname][tree.id].parent = tree;
return this.localTable[fname][tree.id];
} else if (tree.id in this.nameTable) {
/* Defined in nameTable. */
if (this.nameTable[tree.id].args.length === 0) {
/* Defined as name. */
this.nameTable[tree.id].expr.parent = tree;
return this.reduceIfReturnList(this.Evaluator(this.nameTable[tree.id].expr));
} else {
/* Defined as function name. */
throw new EvalError(`calling ${tree.id} function without arguments list.`);
}
} else {
throw new ReferenceError(`'${tree.id}' undefined.`);
}
case 'LIST':
const result = {
type: 'LIST',
list: new Array(tree.list.length),
parent: tree.parent === null ? null : tree,
};
for (let i = 0; i < tree.list.length; i++) {
/* Convert undefined name, defined in word-list command, to word-list command.
* (Null length word-list command) */
if (
tree.list[i].type === 'NAME' &&
!(local && this.localTable[fname] && this.localTable[fname][tree.list[i].id]) &&
!(tree.list[i].id in this.nameTable) &&
commandsTable.indexOf(tree.list[i].id) >= 0
) {
tree.list[i].type = 'CmdWList';
tree.list[i]['args'] = [];
}
tree.list[i].parent = result;
tree.list[i].index = i;
result.list[i] = this.reduceIfReturnList(this.Evaluator(tree.list[i], local, fname));
if (typeof result.list[i].type === 'number') {
this.nameTable['ans'] = { args: [], expr: result.list[i] };
}
}
return result;
case 'RANGE':
tree.start.parent = tree;
tree.stop.parent = tree;
if (tree.stride) {
tree.stride.parent = tree;
}
return this.expandRange(
this.reduceIfReturnList(this.Evaluator(tree.start, local, fname)),
this.reduceIfReturnList(this.Evaluator(tree.stop, local, fname)),
tree.stride ? this.reduceIfReturnList(this.Evaluator(tree.stride, local, fname)) : null,
);
case 'ENDRANGE': {
let parent = tree.parent;
/* Search for 'ARG' node until reach 'ARG' or root node */
while (parent !== null && parent.type !== 'ARG') {
parent = parent.parent;
}
if (
parent &&