UNPKG

expression-evaluation

Version:
496 lines (495 loc) 25.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Expression = void 0; const FunctionDefinition_js_1 = require("./FunctionDefinition.js"); const GlobalFunctions_js_1 = require("./function/GlobalFunctions.js"); const BaseFunctions_js_1 = require("./function/BaseFunctions.js"); const CompositeFunctions_js_1 = require("./function/CompositeFunctions.js"); const MathFunctions_js_1 = require("./function/MathFunctions.js"); const MutationFunctions_js_1 = require("./function/MutationFunctions.js"); const StaticScope_js_1 = require("./StaticScope.js"); const Variable_js_1 = require("./Variable.js"); const Type_js_1 = require("./Type.js"); const ParserState_js_1 = require("./ParserState.js"); const ConstantNode_js_1 = require("./node/ConstantNode.js"); const CallNode_js_1 = require("./node/CallNode.js"); const VariableNode_js_1 = require("./node/VariableNode.js"); const LoopNode_js_1 = require("./node/LoopNode.js"); const ArrayNode_js_1 = require("./node/ArrayNode.js"); const ObjectNode_js_1 = require("./node/ObjectNode.js"); const ProgramNode_js_1 = require("./node/ProgramNode.js"); const FunctionSignature_js_1 = require("./FunctionSignature.js"); const keywords = ['void', 'boolean', 'bool', 'number', 'num', 'buffer', 'buf', 'string', 'str', 'array', 'arr', 'object', 'obj', 'function', 'func', 'variant', 'var', 'if', 'then', 'else', ]; const constants = [ ['NAN', Number.NaN], ['POSINF', Number.POSITIVE_INFINITY], ['NEGINF', Number.NEGATIVE_INFINITY], ['EPSILON', 2.718281828459045], ['PI', 3.141592653589793], ]; const gfunctions = [ ['Or', GlobalFunctions_js_1.funcOr], ['And', GlobalFunctions_js_1.funcAnd], ['Not', GlobalFunctions_js_1.funcNot], ['Sum', GlobalFunctions_js_1.funcSum], ['Min', GlobalFunctions_js_1.funcMin], ['Max', GlobalFunctions_js_1.funcMax], ['Range', GlobalFunctions_js_1.funcRange], ['Chain', GlobalFunctions_js_1.funcChain], ['Merge', GlobalFunctions_js_1.funcMerge], ['Now', GlobalFunctions_js_1.funcNow], ['RandomNumber', GlobalFunctions_js_1.funcRandomNumber], ['RandomInteger', GlobalFunctions_js_1.funcRandomInteger], ['RandomBuffer', GlobalFunctions_js_1.funcRandomBuffer], ['RandomString', GlobalFunctions_js_1.funcRandomString], ]; const mfunctions = [ ['GreaterThan', BaseFunctions_js_1.funcGreaterThan], ['LessThan', BaseFunctions_js_1.funcLessThan], ['GreaterOrEqual', BaseFunctions_js_1.funcGreaterOrEqual], ['LessOrEqual', BaseFunctions_js_1.funcLessOrEqual], ['Equal', BaseFunctions_js_1.funcEqual], ['Unequal', BaseFunctions_js_1.funcNotEqual], ['Like', BaseFunctions_js_1.funcLike], ['Unlike', BaseFunctions_js_1.funcUnlike], ['Coalesce', BaseFunctions_js_1.funcCoalesce], ['Switch', BaseFunctions_js_1.funcSwitch], ['Contains', BaseFunctions_js_1.funcContains], ['StartsWith', BaseFunctions_js_1.funcStartsWith], ['EndsWith', BaseFunctions_js_1.funcEndsWith], ['Alphanum', BaseFunctions_js_1.funcAlphanum], ['Trim', BaseFunctions_js_1.funcTrim], ['TrimStart', BaseFunctions_js_1.funcTrimStart], ['TrimEnd', BaseFunctions_js_1.funcTrimEnd], ['LowerCase', BaseFunctions_js_1.funcLowerCase], ['UpperCase', BaseFunctions_js_1.funcUpperCase], ['Join', BaseFunctions_js_1.funcJoin], ['Split', BaseFunctions_js_1.funcSplit], ['Unique', BaseFunctions_js_1.funcUnique], ['Intersection', BaseFunctions_js_1.funcIntersection], ['Difference', BaseFunctions_js_1.funcDifference], ['Append', CompositeFunctions_js_1.funcAppend], ['Length', CompositeFunctions_js_1.funcLength], ['Slice', CompositeFunctions_js_1.funcSlice], ['Byte', CompositeFunctions_js_1.funcByte], ['Char', CompositeFunctions_js_1.funcChar], ['CharCode', CompositeFunctions_js_1.funcCharCode], ['Entries', CompositeFunctions_js_1.funcEntries], ['Keys', CompositeFunctions_js_1.funcKeys], ['Values', CompositeFunctions_js_1.funcValues], ['At', CompositeFunctions_js_1.funcAt], ['First', CompositeFunctions_js_1.funcFirst], ['Last', CompositeFunctions_js_1.funcLast], ['FirstIndex', CompositeFunctions_js_1.funcFirstIndex], ['LastIndex', CompositeFunctions_js_1.funcLastIndex], ['Any', CompositeFunctions_js_1.funcAny], ['Every', CompositeFunctions_js_1.funcEvery], ['Flatten', CompositeFunctions_js_1.funcFlatten], ['Reverse', CompositeFunctions_js_1.funcReverse], ['Transform', CompositeFunctions_js_1.funcTransform], ['Filter', CompositeFunctions_js_1.funcFilter], ['Reduce', CompositeFunctions_js_1.funcReduce], ['Compose', CompositeFunctions_js_1.funcCompose], ['Add', MathFunctions_js_1.funcAdd], ['Subtract', MathFunctions_js_1.funcSubtract], ['Negate', MathFunctions_js_1.funcNegate], ['Multiply', MathFunctions_js_1.funcMultiply], ['Divide', MathFunctions_js_1.funcDivide], ['Remainder', MathFunctions_js_1.funcRemainder], ['Modulo', MathFunctions_js_1.funcModulo], ['Exponent', MathFunctions_js_1.funcExponent], ['Logarithm', MathFunctions_js_1.funcLogarithm], ['Power', MathFunctions_js_1.funcPower], ['Root', MathFunctions_js_1.funcRoot], ['Abs', MathFunctions_js_1.funcAbs], ['Ceil', MathFunctions_js_1.funcCeil], ['Floor', MathFunctions_js_1.funcFloor], ['Round', MathFunctions_js_1.funcRound], ['ToUniversalTime', MutationFunctions_js_1.funcToUniversalTime], ['FromUniversalTime', MutationFunctions_js_1.funcFromUniversalTime], ['ToLocalTime', MutationFunctions_js_1.funcToLocalTime], ['FromLocalTime', MutationFunctions_js_1.funcFromLocalTime], ['ToUniversalTimeMonthIndex', MutationFunctions_js_1.funcToUniversalTimeMonthIndex], ['ToLocalTimeMonthIndex', MutationFunctions_js_1.funcToLocalTimeMonthIndex], ['ToUniversalTimeWeekdayIndex', MutationFunctions_js_1.funcToUniversalTimeWeekdayIndex], ['ToLocalTimeWeekdayIndex', MutationFunctions_js_1.funcToLocalTimeWeekdayIndex], ['ToTimeString', MutationFunctions_js_1.funcToTimeString], ['FromTimeString', MutationFunctions_js_1.funcFromTimeString], ['ToNumberBuffer', MutationFunctions_js_1.funcToNumberBuffer], ['FromNumberBuffer', MutationFunctions_js_1.funcFromNumberBuffer], ['ToStringBuffer', MutationFunctions_js_1.funcToStringBuffer], ['FromStringBuffer', MutationFunctions_js_1.funcFromStringBuffer], ['ToBooleanString', MutationFunctions_js_1.funcToNumberString], ['FromBooleanString', MutationFunctions_js_1.funcFromNumberString], ['ToNumberString', MutationFunctions_js_1.funcToNumberString], ['FromNumberString', MutationFunctions_js_1.funcFromNumberString], ['ToBufferString', MutationFunctions_js_1.funcToBufferString], ['FromBufferString', MutationFunctions_js_1.funcFromBufferString], ['ToJsonString', MutationFunctions_js_1.funcToJsonString], ['FromJsonString', MutationFunctions_js_1.funcFromJsonString], ['ToText', MutationFunctions_js_1.funcToText], ]; class Expression { static keywords = [...keywords, ...constants.map((c) => c[0]), ...gfunctions.map((f) => f[0])]; _expression; _strict; _root; _variables = new Map(); _constants = new Map(constants); _gfunctions = new Map(gfunctions); _mfunctions = new Map(mfunctions); _scope = new StaticScope_js_1.StaticScope(); /** Creates compiled expression. Any parsed token not recognized as a constant or a function will be compiled as a variable. @param expr Math expression to compile. @param config Optional expected type, strict mode, variable types, constant values and functions to add for the compilation. If expected type is provided then expression return type is matched against it. If strict mode is set then undeclared variables will not be allowed in expression. */ constructor(expr, config) { this._expression = expr; const type = config?.type ?? Type_js_1.typeUnknown; this._strict = config?.strict ?? false; if (config?.variables) { for (const v in config.variables) { this._variables.set(v, new Variable_js_1.Variable(config.variables[v])); } } if (config?.constants) { for (const c in config.constants) { this._constants.set(c, config.constants[c]); } } if (config?.functions) { for (const f in config.constants) { this._gfunctions.set(f, new FunctionDefinition_js_1.FunctionDefinition(config.functions[f].func, config.functions[f].type, config.functions[f].argTypes, config.functions[f].minArity, config.functions[f].maxArity, config.functions[f].typeInference, config.functions[f].pure ?? true)); } } const state = new ParserState_js_1.ParserState(this._expression); this._root = this.program(state.next(), this._scope); if (!state.isVoid) { state.throwError('unexpected expression token or expression end'); } this._root = this._root.compile(type); } /** Returns compiled expression return value type. */ get type() { return this._root.type; } /** Returns string representing parsed node tree structure. @returns Parsed expression string. */ toString() { return this._root.toString(); } /** Returns record with compiled variable names and expected types. @returns Record with variable names and types. */ variables() { const types = {}; const variables = this._scope.variables(); for (const name in variables) { types[name] = variables[name].type; } return types; } /** Evaluates compiled expression using provided variable values. @param values Record with variable names and values. @returns Calculated value. */ evaluate(values) { const variables = this._scope.variables(); for (const name in variables) { if (!Object.prototype.hasOwnProperty.call(values, name)) { this._root.frame(name).throwError(`undefined variable ${name}:\n`); } const variable = variables[name]; const value = values?.[name] ?? undefined; if (!variable.type.reduce(Type_js_1.Type.of(value))) { this._root.frame(name).throwError(`unexpected type ${Type_js_1.Type.of(value)} for variable ${name} of type ${variable.type}:\n`); } variable.value = value; } return this._root.evaluate(); } program(state, scope) { const frame = state.frame(); const nodes = [this.unit(state, scope)]; while (state.isCommaSeparator) { nodes.push(this.unit(state.next(), scope)); } return new ProgramNode_js_1.ProgramNode(frame.ends(state.end), nodes); } unit(state, scope) { return this.loop(state, scope); } loop(state, scope) { let node = this.condition(state, scope); while (state.isCycle) { node = new LoopNode_js_1.LoopNode(state.frame(), node, this.condition(state.next(), scope)); } return node; } condition(state, scope) { const frame = state.frame(); let node = this.disjunction(state, scope); if (state.operator === BaseFunctions_js_1.funcSwitch) { const subnode = this.unit(state.next(), scope); if (!state.isColonSeparator) { state.throwError('missing else conditional clause'); } node = this.call(frame.ends(state.end), BaseFunctions_js_1.funcSwitch, [node, subnode, this.unit(state.next(), scope)]); } return node; } disjunction(state, scope) { let node = this.conjunction(state, scope); while (state.operator === GlobalFunctions_js_1.funcOr) { node = this.call(state.frame(), state.operator, [node, this.conjunction(state.next(), scope)]); } return node; } conjunction(state, scope) { let node = this.comparison(state, scope); while (state.operator === GlobalFunctions_js_1.funcAnd) { node = this.call(state.frame(), state.operator, [node, this.comparison(state.next(), scope)]); } return node; } comparison(state, scope) { const frame = state.frame(); let not = false; while (state.operator === GlobalFunctions_js_1.funcNot) { not = !not; frame.starts(state.start); state.next(); } let node = this.aggregate(state, scope); while (state.operator === BaseFunctions_js_1.funcGreaterThan || state.operator === BaseFunctions_js_1.funcLessThan || state.operator === BaseFunctions_js_1.funcGreaterOrEqual || state.operator === BaseFunctions_js_1.funcLessOrEqual || state.operator === BaseFunctions_js_1.funcEqual || state.operator === BaseFunctions_js_1.funcNotEqual) { node = this.call(state.frame(), state.operator, [node, this.aggregate(state.next(), scope)]); } if (not) { node = this.call(frame.ends(state.end), GlobalFunctions_js_1.funcNot, [node]); } return node; } aggregate(state, scope) { let node = this.product(state, scope); while (state.operator === CompositeFunctions_js_1.funcAppend || state.operator === MathFunctions_js_1.funcAdd || state.operator === MathFunctions_js_1.funcSubtract) { node = this.call(state.frame(), state.operator, [node, this.product(state.next(), scope)]); } return node; } product(state, scope) { let node = this.factor(state, scope); while (state.operator === MathFunctions_js_1.funcMultiply || state.operator === MathFunctions_js_1.funcDivide || state.operator === MathFunctions_js_1.funcRemainder) { node = this.call(state.frame(), state.operator, [node, this.factor(state.next(), scope)]); } return node; } factor(state, scope) { const frame = state.frame(); let neg = false; while (state.operator === MathFunctions_js_1.funcSubtract) { neg = !neg; frame.starts(state.start); state.next(); } let node = this.coalescence(state, scope); while (state.operator === MathFunctions_js_1.funcPower) { node = this.call(state.frame(), state.operator, [node, this.coalescence(state.next(), scope)]); } if (neg) { node = this.call(frame.ends(state.end), MathFunctions_js_1.funcNegate, [node]); } return node; } coalescence(state, scope) { let node = this.accessor(state, scope); while (state.operator === BaseFunctions_js_1.funcCoalesce) { node = this.call(state.frame(), state.operator, [node, this.accessor(state.next(), scope)]); } return node; } accessor(state, scope) { let node = this.term(state, scope); while (state.operator === CompositeFunctions_js_1.funcAt || state.isParenthesesOpen || state.isBracketsOpen) { const frame = state.frame(); if (state.operator === CompositeFunctions_js_1.funcAt) { state.next(); if (state.isLiteral && (typeof state.literalValue === 'string' || typeof state.literalValue === 'number')) { node = this.call(frame.ends(state.end), CompositeFunctions_js_1.funcAt, [node, new ConstantNode_js_1.ConstantNode(state, state.literalValue)]); state.next(); } else if (state.isToken) { frame.ends(state.end); const mfunction = this._mfunctions.get(state.token) ?? this._gfunctions.get(state.token); if (mfunction) { if (state.next().isParenthesesOpen) { const subnodes = [node]; while (!state.next().isParenthesesClose) { subnodes.push(this.unit(state, scope)); if (!state.isCommaSeparator) { break; } } if (!state.isParenthesesClose) { state.throwError('missing closing method function parentheses'); } frame.ends(state.end); node = this.call(frame, mfunction, subnodes); state.next(); } else { node = this.call(frame, mfunction, [node]); } } else { node = this.call(frame, CompositeFunctions_js_1.funcAt, [node, new ConstantNode_js_1.ConstantNode(state, state.token)]); state.next(); } } else { state.throwError('missing array or object index'); } } else if (state.isParenthesesOpen) { const subnodes = []; while (!state.next().isParenthesesClose) { subnodes.push(this.unit(state, scope)); if (!state.isCommaSeparator) { break; } } if (!state.isParenthesesClose) { state.throwError('missing closing function parentheses'); } node = new CallNode_js_1.CallNode(frame.ends(state.end), node, subnodes); state.next(); } else if (state.isBracketsOpen) { node = this.call(frame, CompositeFunctions_js_1.funcAt, [node, this.unit(state.next(), scope)]); if (!state.isBracketsClose) { state.throwError('missing closing index brackets'); } state.next(); } } return node; } term(state, scope) { if (state.isLiteral) { const frame = state.frame(); const constant = state.literalValue; state.next(); return new ConstantNode_js_1.ConstantNode(frame, constant); } else if (state.isToken) { const constant = this._constants.get(state.token); if (constant != null) { const frame = state.frame(); state.next(); return new ConstantNode_js_1.ConstantNode(frame, constant); } const gfunction = this._gfunctions.get(state.token); if (gfunction != null) { const frame = state.frame(); state.next(); return new ConstantNode_js_1.ConstantNode(frame, gfunction.evaluate, gfunction.signature); } return this.variable(state, scope); } else if (state.isType) { let type = state.type; if (state.next().isOptionalType) { type = type.toOptional(); state.next(); } if (state.isToken) { return this.variable(state, scope, type); } return this.subroutine(state, scope, type); } else if (state.isScope) { return this.subroutine(state, scope); } else if (state.isBracesOpen) { const node = this.program(state.next(), scope); if (!state.isBracesClose) { state.throwError('missing closing braces'); } state.next(); return node; } else if (state.isBracesClose) { state.throwError('unexpected closing braces'); } else if (state.isParenthesesOpen) { const node = this.unit(state.next(), scope); if (!state.isParenthesesClose) { state.throwError('missing closing parentheses'); } state.next(); return node; } else if (state.isParenthesesClose) { state.throwError('unexpected closing parentheses'); } else if (state.isBracketsOpen) { const frame = state.frame(); const checkEmptyState = state.clone(); if (checkEmptyState.next().isBracketsClose) { return new ConstantNode_js_1.ConstantNode(frame.ends(state.next().next().end), []); } else if (checkEmptyState.isColonSeparator && checkEmptyState.next().isBracketsClose) { return new ConstantNode_js_1.ConstantNode(frame.ends(state.next().next().next().end), {}); } const subnodes = []; let index = 0; while (!state.next().isBracketsClose) { const node = this.unit(state, scope); if (state.isColonSeparator) { subnodes.push([node, this.unit(state.next(), scope)]); } else { subnodes.push([index++, node]); } if (!state.isCommaSeparator) { break; } } if (!state.isBracketsClose) { state.throwError('missing closing brackets'); } frame.ends(state.end); state.next(); if (subnodes.every(([k,]) => typeof k === 'number')) { return new ArrayNode_js_1.ArrayNode(frame, subnodes.map(([, v]) => v)); } return new ObjectNode_js_1.ObjectNode(frame, subnodes.map(([k, v]) => [typeof k === 'number' ? new ConstantNode_js_1.ConstantNode(v, String(k)) : k, v])); } else if (state.isBracketsClose) { state.throwError('unexpected closing brackets'); } else if (state.isVoid) { state.throwError('unexpected end of expression'); } state.throwError('unexpected expression token'); } variable(state, scope, type) { let variable = undefined; if (type) { if (scope.has(state.token)) { state.throwError(`variable ${state.token} redefinition`); } variable = new Variable_js_1.Variable(type); scope.local(state.token, variable); } else { variable = scope.get(state.token); if (variable == null) { variable = this._variables.get(state.token); if (variable == null) { if (this._strict) { state.throwError(`undefined variable ${state.token} in strict mode`); } else { variable = new Variable_js_1.Variable(); } } scope.global(state.token, variable); } } const frame = state.frame(); return new VariableNode_js_1.VariableNode(frame.ends(state.end), variable, state.next().isAssignment ? state.assignmentOperator ? this.call(frame.ends(state.end), state.assignmentOperator, [new VariableNode_js_1.VariableNode(frame.ends(state.end), variable), this.unit(state.next(), scope)]) : this.unit(state.next(), scope) : undefined); } subroutine(state, scope, type) { const frame = state.frame(); const variables = new Map(); if (type) { if (!state.isParenthesesOpen) { state.throwError('missing opening function type parentheses'); } while (!state.next().isParenthesesClose) { if (!state.isType) { state.throwError('missing function argument type'); } let argType = state.type; if (state.next().isOptionalType) { argType = argType.toOptional(); state.next(); } if (!state.isToken) { state.throwError('missing function argument name'); } const token = state.token; if (scope.get(token)) { state.throwError('variable redefinition'); } variables.set(token, new Variable_js_1.Variable(argType)); if (!state.next().isCommaSeparator) { break; } } if (!state.isParenthesesClose) { state.throwError('missing closing function type parentheses'); } if (!state.next().isScope) { state.throwError('missing function scope'); } } const args = Array.from(variables.values()); const subnode = this.unit(state.next(), scope.subscope(variables)); const value = (...values) => { args.forEach((arg, ix) => arg.value = values[ix]); return subnode.evaluate(); }; return new ConstantNode_js_1.ConstantNode(frame.ends(state.end), value, type ? new FunctionSignature_js_1.FunctionSignature(type, args.map((v) => v.type)) : FunctionSignature_js_1.FunctionSignature.unknown, subnode); } call(frame, func, subnodes) { return new CallNode_js_1.CallNode(frame, new ConstantNode_js_1.ConstantNode(frame, func.evaluate, func.signature), subnodes); } } exports.Expression = Expression;