From b78517b61fa6b21f08f03bac4abc260a0804e337 Mon Sep 17 00:00:00 2001 From: Stefan Zermatten Date: Sun, 3 Oct 2021 13:54:17 +0200 Subject: [PATCH] Finished migrating parser to be object orientation free. All tests pass --- .../linkCalculationDependencies.js | 7 +- .../parseCalculationFields.js | 6 +- .../computeComputation/computeCalculations.js | 13 ++- app/imports/api/properties/Constants.js | 20 ++--- app/imports/parser/ResolveContext.js | 20 ----- app/imports/parser/TextField.vue | 38 +++++++++ app/imports/parser/functions.js | 4 +- app/imports/parser/grammar.js | 47 ++++------ app/imports/parser/grammar.ne | 45 ++++------ app/imports/parser/parseTree/IndexNode.js | 64 -------------- .../parser/parseTree/NotOperatorNode.js | 34 -------- app/imports/parser/parseTree/OperatorNode.js | 71 ---------------- .../parser/parseTree/ParenthesisNode.js | 30 ------- app/imports/parser/parseTree/ParseNode.js | 43 ---------- app/imports/parser/parseTree/RollArrayNode.js | 22 ----- app/imports/parser/parseTree/RollNode.js | 70 --------------- app/imports/parser/parseTree/SymbolNode.js | 53 ------------ .../parser/parseTree/UnaryOperatorNode.js | 40 --------- app/imports/parser/parseTree/_index.js | 26 ++++++ app/imports/parser/parseTree/accessor.js | 5 +- app/imports/parser/parseTree/array.js | 6 +- app/imports/parser/parseTree/call.js | 16 ++-- app/imports/parser/parseTree/constant.js | 11 ++- app/imports/parser/parseTree/error.js | 6 +- app/imports/parser/parseTree/if.js | 31 ++----- app/imports/parser/parseTree/index.js | 85 +++++++++---------- app/imports/parser/parseTree/not.js | 37 ++++++++ app/imports/parser/parseTree/operator.js | 76 +++++++++++++++++ app/imports/parser/parseTree/parenthesis.js | 34 ++++++++ app/imports/parser/parseTree/roll.js | 82 ++++++++++++++++++ app/imports/parser/parseTree/rollArray.js | 35 ++++++++ app/imports/parser/parseTree/symbol.js | 61 +++++++++++++ app/imports/parser/parseTree/unaryOperator.js | 46 ++++++++++ app/imports/parser/parser.js | 14 --- app/imports/parser/resolve.js | 45 +++++++--- 35 files changed, 589 insertions(+), 654 deletions(-) delete mode 100644 app/imports/parser/ResolveContext.js create mode 100644 app/imports/parser/TextField.vue delete mode 100644 app/imports/parser/parseTree/IndexNode.js delete mode 100644 app/imports/parser/parseTree/NotOperatorNode.js delete mode 100644 app/imports/parser/parseTree/OperatorNode.js delete mode 100644 app/imports/parser/parseTree/ParenthesisNode.js delete mode 100644 app/imports/parser/parseTree/ParseNode.js delete mode 100644 app/imports/parser/parseTree/RollArrayNode.js delete mode 100644 app/imports/parser/parseTree/RollNode.js delete mode 100644 app/imports/parser/parseTree/SymbolNode.js delete mode 100644 app/imports/parser/parseTree/UnaryOperatorNode.js create mode 100644 app/imports/parser/parseTree/not.js create mode 100644 app/imports/parser/parseTree/operator.js create mode 100644 app/imports/parser/parseTree/parenthesis.js create mode 100644 app/imports/parser/parseTree/roll.js create mode 100644 app/imports/parser/parseTree/rollArray.js create mode 100644 app/imports/parser/parseTree/symbol.js create mode 100644 app/imports/parser/parseTree/unaryOperator.js diff --git a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js index 69f747ee..0e91f833 100644 --- a/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js +++ b/app/imports/api/engine/computation/buildComputation/linkCalculationDependencies.js @@ -1,6 +1,5 @@ -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; -import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; import findAncestorByType from '/imports/api/engine/computation/utility/findAncestorByType.js'; +import { traverse } from '/imports/parser/resolve.js'; export default function linkCalculationDependencies(dependencyGraph, prop, {propsById}){ prop._computationDetails.calculations.forEach(calcObj => { @@ -9,9 +8,9 @@ export default function linkCalculationDependencies(dependencyGraph, prop, {prop // ancestors: {} //this gets added if there are resolved ancestors }; // Traverse the parsed calculation looking for variable names - calcObj._parsedCalculation.traverse(node => { + traverse(calcObj._parsedCalculation, node => { // Skip nodes that aren't symbols or accessors - if (!(node instanceof SymbolNode || node instanceof AccessorNode)) return; + if (node.parseType !== 'symbol' && node.parseType !== 'accessor') return; // Link ancestor references as direct property dependencies if (node.name[0] === '#'){ let ancestorProp = getAncestorProp( diff --git a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js index 72e698f3..1d824e85 100644 --- a/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js +++ b/app/imports/api/engine/computation/buildComputation/parseCalculationFields.js @@ -1,8 +1,8 @@ import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; import { prettifyParseError, parse } from '/imports/parser/parser.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; import applyFnToKey from '/imports/api/engine/computation/utility/applyFnToKey.js'; -import { get, unset } from 'lodash'; +import { get } from 'lodash'; +import errorNode from '/imports/parser/parseTree/error.js' export default function parseCalculationFields(prop, schemas){ discoverInlineCalculationFields(prop, schemas); @@ -66,6 +66,6 @@ function parseCalculation(calcObj){ calcObj.errors ? calcObj.errors.push(error) : calcObj.errors = [error]; - calcObj._parsedCalculation = new ErrorNode({error}); + calcObj._parsedCalculation = errorNode.create({error}); } } diff --git a/app/imports/api/engine/computation/computeComputation/computeCalculations.js b/app/imports/api/engine/computation/computeComputation/computeCalculations.js index 4034b5eb..ccbaf79f 100644 --- a/app/imports/api/engine/computation/computeComputation/computeCalculations.js +++ b/app/imports/api/engine/computation/computeComputation/computeCalculations.js @@ -1,7 +1,5 @@ -import { CompilationContext } from '/imports/parser/parser.js'; +import resolve, { toString } from '/imports/parser/resolve.js'; import INLINE_CALCULATION_REGEX from '/imports/constants/INLINE_CALCULTION_REGEX.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; export default function computeCalculations(computation, node){ if (!node.data) return; @@ -15,17 +13,16 @@ export default function computeCalculations(computation, node){ } function evaluateCalculation(calculation, scope){ - const context = new CompilationContext(); const parseNode = calculation._parsedCalculation; const fn = calculation._parseLevel; const calculationScope = {...calculation._localScope, ...scope}; - const resultNode = parseNode[fn](calculationScope, context); - if (resultNode instanceof ConstantNode){ + const {result: resultNode, context} = resolve(fn, parseNode, calculationScope); + if (resultNode.parseType === 'constant'){ calculation.value = resultNode.value; - } else if (resultNode instanceof ErrorNode){ + } else if (resultNode.parseType === 'error'){ calculation.value = null; } else { - calculation.value = resultNode.toString(); + calculation.value = toString(resultNode); } if (calculation.errors){ calculation.errors = [...calculation.errors, ...context.errors] diff --git a/app/imports/api/properties/Constants.js b/app/imports/api/properties/Constants.js index 4b1f382f..0995ccd9 100644 --- a/app/imports/api/properties/Constants.js +++ b/app/imports/api/properties/Constants.js @@ -3,12 +3,10 @@ import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js'; import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js'; import { parse, - CompilationContext, prettifyParseError, } from '/imports/parser/parser.js'; -import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; -import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; +import resolve, { Context, traverse } from '/imports/parser/resolve.js'; /* * Constants are primitive values that can be used elsewhere in computations @@ -50,12 +48,9 @@ let ConstantSchema = new SimpleSchema({ // Any existing errors will result in an early failure if (context && context.errors.length) return context.errors; // Ban variables in constants if necessary - result && result.traverse(node => { - if (node instanceof SymbolNode || node instanceof AccessorNode){ - context.storeError({ - type: 'error', - message: 'Variables can\'t be used to define a constant' - }); + result && traverse(result, node => { + if (node.parseType === 'symbol' || node.parseType === 'accessor'){ + context.error('Variables can\'t be used to define a constant'); } }); return context && context.errors || []; @@ -67,7 +62,7 @@ let ConstantSchema = new SimpleSchema({ }); function parseString(string, fn = 'compile'){ - let context = new CompilationContext(); + let context = new Context(); if (!string){ return {result: string, context}; } @@ -78,10 +73,11 @@ function parseString(string, fn = 'compile'){ node = parse(string); } catch (e) { let message = prettifyParseError(e); - context.storeError({type: 'error', message}); + context.error(message); return {context}; } - let result = node[fn]({/*empty scope*/}, context); + if (!node) return {context}; + let {result} = resolve(fn, node, {/*empty scope*/}, context); return {result, context} } diff --git a/app/imports/parser/ResolveContext.js b/app/imports/parser/ResolveContext.js deleted file mode 100644 index 3edd2fa8..00000000 --- a/app/imports/parser/ResolveContext.js +++ /dev/null @@ -1,20 +0,0 @@ -export default class Context { - constructor({errors = [], rolls = []}){ - this.errors = errors; - this.rolls = rolls; - } - error(e){ - if (!e) return; - if (typeof e === 'string'){ - this.errors.push({ - type: 'error', - message: e, - }); - } else { - this.errors.push(e); - } - } - roll(r){ - this.rolls.push(r); - } -} diff --git a/app/imports/parser/TextField.vue b/app/imports/parser/TextField.vue new file mode 100644 index 00000000..9c9f0919 --- /dev/null +++ b/app/imports/parser/TextField.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index b1ec5a91..9213665e 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -1,5 +1,3 @@ -import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; - export default { 'abs': { comment: 'Returns the absolute value of a number', @@ -101,7 +99,7 @@ export default { {input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'}, {input: 'tableLookup([100, 300], 594)', result: '2'}, ], - arguments: [ArrayNode, 'number'], + arguments: ['array', 'number'], resultType: 'number', fn: function tableLookup(arrayNode, number){ for(let i in arrayNode.values){ diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index 8e29fb16..064fc2f2 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -1,19 +1,8 @@ -// Generated automatically by nearley, version 2.16.0 +// Generated automatically by nearley, version 2.20.1 // http://github.com/Hardmath123/nearley function id(x) { return x[0]; } - import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; - import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; - import CallNode from '/imports/parser/parseTree/CallNode.js'; - import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - import IfNode from '/imports/parser/parseTree/IfNode.js'; - import IndexNode from '/imports/parser/parseTree/IndexNode.js'; - import OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; - import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js'; - import RollNode from '/imports/parser/parseTree/RollNode.js'; - import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; - import UnaryOperatorNode from '/imports/parser/parseTree/UnaryOperatorNode.js'; - import NotOperatorNode from '/imports/parser/parseTree/NotOperatorNode.js'; + import node from './parseTree/_index.js'; import moo from 'moo'; @@ -53,7 +42,7 @@ function id(x) { return x[0]; } function nuller() { return null; } function operator([left, _1, operator, _2, right], fn){ - return new OperatorNode({ + return node.operator.create({ left, right, operator: operator.value, @@ -65,7 +54,7 @@ let ParserRules = [ {"name": "spacedExpression", "symbols": ["_", "expression", "_"], "postprocess": d => d[1]}, {"name": "expression", "symbols": ["ifStatement"], "postprocess": id}, {"name": "ifStatement", "symbols": ["orExpression", "_", (lexer.has("ifOperator") ? {type: "ifOperator"} : ifOperator), "_", "orExpression", "_", (lexer.has("elseOperator") ? {type: "elseOperator"} : elseOperator), "_", "ifStatement"], "postprocess": - d => new IfNode({condition: d[0], consequent: d[4], alternative: d[8]}) + d => node.if.create({condition: d[0], consequent: d[4], alternative: d[8]}) }, {"name": "ifStatement", "symbols": ["orExpression"], "postprocess": id}, {"name": "orExpression", "symbols": ["orExpression", "_", (lexer.has("orOperator") ? {type: "orOperator"} : orOperator), "_", "andExpression"], "postprocess": d => operator(d, 'or')}, @@ -82,18 +71,18 @@ let ParserRules = [ {"name": "remainderExpression", "symbols": ["multiplicativeExpression"], "postprocess": id}, {"name": "multiplicativeExpression", "symbols": ["multiplicativeExpression", "_", (lexer.has("multiplicativeOperator") ? {type: "multiplicativeOperator"} : multiplicativeOperator), "_", "rollExpression"], "postprocess": d => operator(d, 'multiply')}, {"name": "multiplicativeExpression", "symbols": ["rollExpression"], "postprocess": id}, - {"name": "rollExpression", "symbols": ["rollExpression", "_", (lexer.has("diceOperator") ? {type: "diceOperator"} : diceOperator), "_", "exponentExpression"], "postprocess": d => new RollNode({left: d[0], right: d[4]})}, + {"name": "rollExpression", "symbols": ["rollExpression", "_", (lexer.has("diceOperator") ? {type: "diceOperator"} : diceOperator), "_", "exponentExpression"], "postprocess": d => node.roll.create({left: d[0], right: d[4]})}, {"name": "rollExpression", "symbols": ["singleRollExpression"], "postprocess": id}, - {"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "singleRollExpression"], "postprocess": d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]})}, + {"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "singleRollExpression"], "postprocess": d => node.roll.create({left: node.constant.create({value: 1}), right: d[2]})}, {"name": "singleRollExpression", "symbols": ["exponentExpression"], "postprocess": id}, {"name": "exponentExpression", "symbols": ["callExpression", "_", (lexer.has("exponentOperator") ? {type: "exponentOperator"} : exponentOperator), "_", "exponentExpression"], "postprocess": d => operator(d, 'exponent')}, {"name": "exponentExpression", "symbols": ["unaryExpression"], "postprocess": id}, - {"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => new UnaryOperatorNode({operator: d[0].value, right: d[2]})}, + {"name": "unaryExpression", "symbols": [(lexer.has("additiveOperator") ? {type: "additiveOperator"} : additiveOperator), "_", "unaryExpression"], "postprocess": d => node.unaryOperator.create({operator: d[0].value, right: d[2]})}, {"name": "unaryExpression", "symbols": ["notExpression"], "postprocess": id}, - {"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => new NotOperatorNode({right: d[2]})}, + {"name": "notExpression", "symbols": [(lexer.has("notOperator") ? {type: "notOperator"} : notOperator), "_", "notExpression"], "postprocess": d => node.notOperator.create({right: d[2]})}, {"name": "notExpression", "symbols": ["callExpression"], "postprocess": id}, {"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess": - d => new CallNode ({functionName: d[0].name, args: d[2]}) + d => node.call.create({functionName: d[0].name, args: d[2]}) }, {"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id}, {"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, @@ -105,7 +94,7 @@ let ParserRules = [ {"name": "arguments", "symbols": [{"literal":"("}, "_", "arguments$ebnf$1", "arguments$ebnf$2", "_", {"literal":")"}], "postprocess": d => [d[2], ...d[3]] }, - {"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => new IndexNode ({array: d[0], index: d[3]})}, + {"name": "indexExpression", "symbols": ["arrayExpression", {"literal":"["}, "_", "expression", "_", {"literal":"]"}], "postprocess": d => node.index.create({array: d[0], index: d[3]})}, {"name": "indexExpression", "symbols": ["arrayExpression"], "postprocess": id}, {"name": "arrayExpression$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, {"name": "arrayExpression$ebnf$1", "symbols": ["arrayExpression$ebnf$1$subexpression$1"], "postprocess": id}, @@ -114,10 +103,10 @@ let ParserRules = [ {"name": "arrayExpression$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]}, {"name": "arrayExpression$ebnf$2", "symbols": ["arrayExpression$ebnf$2", "arrayExpression$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, {"name": "arrayExpression", "symbols": [{"literal":"["}, "_", "arrayExpression$ebnf$1", "arrayExpression$ebnf$2", "_", {"literal":"]"}], "postprocess": - d => new ArrayNode({values: d[2] ? [d[2], ...d[3]] : []}) + d => node.array.create({values: d[2] ? [d[2], ...d[3]] : []}) }, {"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id}, - {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => new ParenthesisNode({content: d[2]})}, + {"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => node.parenthesis.create({content: d[2]})}, {"name": "parenthesizedExpression", "symbols": ["accessorExpression"], "postprocess": id}, {"name": "accessorExpression$subexpression$1", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[0].value}, {"name": "accessorExpression$ebnf$1$subexpression$1", "symbols": [{"literal":"."}, (lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[1].value}, @@ -125,18 +114,18 @@ let ParserRules = [ {"name": "accessorExpression$ebnf$1$subexpression$2", "symbols": [{"literal":"."}, (lexer.has("name") ? {type: "name"} : name)], "postprocess": d => d[1].value}, {"name": "accessorExpression$ebnf$1", "symbols": ["accessorExpression$ebnf$1", "accessorExpression$ebnf$1$subexpression$2"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}}, {"name": "accessorExpression", "symbols": ["accessorExpression$subexpression$1", "accessorExpression$ebnf$1"], "postprocess": - d=> new AccessorNode({name: d[0], path: d[1]}) + d=> node.accessor.create({name: d[0], path: d[1]}) }, {"name": "accessorExpression", "symbols": ["valueExpression"], "postprocess": id}, {"name": "valueExpression", "symbols": ["name"], "postprocess": id}, {"name": "valueExpression", "symbols": ["number"], "postprocess": id}, {"name": "valueExpression", "symbols": ["string"], "postprocess": id}, {"name": "valueExpression", "symbols": ["boolean"], "postprocess": id}, - {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode({value: +d[0].value, type: 'number'})}, - {"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => new SymbolNode({name: d[0].value})}, - {"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => new ConstantNode({value: d[0].value, type: 'string'})}, - {"name": "boolean", "symbols": [{"literal":"true"}], "postprocess": d => new ConstantNode({value: true, type: 'boolean'})}, - {"name": "boolean", "symbols": [{"literal":"false"}], "postprocess": d => new ConstantNode({value: false, type: 'boolean'})}, + {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => node.constant.create({value: +d[0].value})}, + {"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => node.symbol.create({name: d[0].value})}, + {"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => node.constant.create({value: d[0].value})}, + {"name": "boolean", "symbols": [{"literal":"true"}], "postprocess": d => node.constant.create({value: true})}, + {"name": "boolean", "symbols": [{"literal":"false"}], "postprocess": d => node.constant.create({value: false})}, {"name": "_", "symbols": []}, {"name": "_", "symbols": [(lexer.has("space") ? {type: "space"} : space)], "postprocess": nuller} ]; diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index cd278bab..82681659 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -1,17 +1,6 @@ @preprocessor esmodule @{% - import AccessorNode from '/imports/parser/parseTree/AccessorNode.js'; - import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; - import CallNode from '/imports/parser/parseTree/CallNode.js'; - import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - import IfNode from '/imports/parser/parseTree/IfNode.js'; - import IndexNode from '/imports/parser/parseTree/IndexNode.js'; - import OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; - import ParenthesisNode from '/imports/parser/parseTree/ParenthesisNode.js'; - import RollNode from '/imports/parser/parseTree/RollNode.js'; - import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; - import UnaryOperatorNode from '/imports/parser/parseTree/UnaryOperatorNode.js'; - import NotOperatorNode from '/imports/parser/parseTree/NotOperatorNode.js'; + import node from './parseTree/_index.js'; import moo from 'moo'; @@ -51,7 +40,7 @@ function nuller() { return null; } function operator([left, _1, operator, _2, right], fn){ - return new OperatorNode({ + return node.operator.create({ left, right, operator: operator.value, @@ -71,7 +60,7 @@ expression -> ifStatement -> orExpression _ %ifOperator _ orExpression _ %elseOperator _ ifStatement {% - d => new IfNode({condition: d[0], consequent: d[4], alternative: d[8]}) + d => node.if.create({condition: d[0], consequent: d[4], alternative: d[8]}) %} | orExpression {% id %} @@ -104,11 +93,11 @@ multiplicativeExpression -> | rollExpression {% id %} rollExpression -> - rollExpression _ %diceOperator _ exponentExpression {% d => new RollNode({left: d[0], right: d[4]}) %} + rollExpression _ %diceOperator _ exponentExpression {% d => node.roll.create({left: d[0], right: d[4]}) %} | singleRollExpression {% id %} singleRollExpression -> - "d" _ singleRollExpression {% d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]}) %} + "d" _ singleRollExpression {% d => node.roll.create({left: node.constant.create({value: 1}), right: d[2]}) %} | exponentExpression {% id %} exponentExpression -> @@ -116,16 +105,16 @@ exponentExpression -> | unaryExpression {% id %} unaryExpression -> - %additiveOperator _ unaryExpression {% d => new UnaryOperatorNode({operator: d[0].value, right: d[2]})%} + %additiveOperator _ unaryExpression {% d => node.unaryOperator.create({operator: d[0].value, right: d[2]})%} | notExpression {% id %} notExpression -> - %notOperator _ notExpression {% d => new NotOperatorNode({right: d[2]})%} + %notOperator _ notExpression {% d => node.notOperator.create({right: d[2]})%} | callExpression {% id %} callExpression -> name _ arguments {% - d => new CallNode ({functionName: d[0].name, args: d[2]}) + d => node.call.create({functionName: d[0].name, args: d[2]}) %} | indexExpression {% id %} @@ -135,22 +124,22 @@ arguments -> %} indexExpression -> - arrayExpression "[" _ expression _ "]" {% d => new IndexNode ({array: d[0], index: d[3]}) %} + arrayExpression "[" _ expression _ "]" {% d => node.index.create({array: d[0], index: d[3]}) %} | arrayExpression {% id %} arrayExpression -> "[" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ "]" {% - d => new ArrayNode({values: d[2] ? [d[2], ...d[3]] : []}) + d => node.array.create({values: d[2] ? [d[2], ...d[3]] : []}) %} | parenthesizedExpression {% id %} parenthesizedExpression -> - "(" _ expression _ ")" {% d => new ParenthesisNode({content: d[2]}) %} + "(" _ expression _ ")" {% d => node.parenthesis.create({content: d[2]}) %} | accessorExpression {% id %} accessorExpression -> (%name {% d => d[0].value %}) ( "." %name {% d => d[1].value %} ):+ {% - d=> new AccessorNode({name: d[0], path: d[1]}) + d=> node.accessor.create({name: d[0], path: d[1]}) %} | valueExpression {% id %} @@ -162,17 +151,17 @@ valueExpression -> # A number or a function of a number number -> - %number {% d => new ConstantNode({value: +d[0].value, type: 'number'}) %} + %number {% d => node.constant.create({value: +d[0].value}) %} name -> - %name {% d => new SymbolNode({name: d[0].value}) %} + %name {% d => node.symbol.create({name: d[0].value}) %} string -> - %string {% d => new ConstantNode({value: d[0].value, type: 'string'}) %} + %string {% d => node.constant.create({value: d[0].value}) %} boolean -> - "true" {% d => new ConstantNode({value: true, type: 'boolean'}) %} -| "false" {% d => new ConstantNode({value: false, type: 'boolean'}) %} + "true" {% d => node.constant.create({value: true}) %} +| "false" {% d => node.constant.create({value: false}) %} _ -> null diff --git a/app/imports/parser/parseTree/IndexNode.js b/app/imports/parser/parseTree/IndexNode.js deleted file mode 100644 index fdd6c6c6..00000000 --- a/app/imports/parser/parseTree/IndexNode.js +++ /dev/null @@ -1,64 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; - -export default class IndexNode extends ParseNode { - constructor({array, index}) { - super(...arguments); - this.array = array; - this.index = index; - } - resolve(fn, scope, context){ - let index = this.index[fn](scope, context); - let array = this.array[fn](scope, context); - - if (index.isInteger && array instanceof ArrayNode){ - if (index.value < 1 || index.value > array.values.length){ - if (context){ - context.storeError({ - type: 'warning', - message: `Index of ${index.value} is out of range for an array` + - ` of length ${array.values.length}`, - }); - } - } - let selection = array.values[index.value - 1]; - if (selection){ - let result = selection[fn](scope, context); - return result; - } - } else if (fn === 'reduce'){ - if (!(array instanceof ArrayNode)){ - return new ErrorNode({ - node: this, - error: 'Can not get the index of a non-array node: ' + - this.array.toString() + ' = ' + array.toString(), - context, - }); - } else if (!index.isInteger){ - return new ErrorNode({ - node: this, - error: array.toString() + ' is not an integer index of the array', - context, - }); - } - } - return new IndexNode({ - index, - array, - previousNodes: [this], - }); - } - toString(){ - return `${this.array.toString()}[${this.index.toString()}]`; - } - traverse(fn){ - fn(this); - this.array.traverse(fn); - this.index.traverse(fn); - } - replaceChildren(fn){ - this.array = this.array.replaceNodes(fn); - this.index = this.index.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/NotOperatorNode.js b/app/imports/parser/parseTree/NotOperatorNode.js deleted file mode 100644 index 47aca511..00000000 --- a/app/imports/parser/parseTree/NotOperatorNode.js +++ /dev/null @@ -1,34 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class NotOperatorNode extends ParseNode { - constructor({right}) { - super(...arguments); - this.right = right; - } - resolve(fn, scope, context){ - let rightNode = this.right[fn](scope, context); - if (!(rightNode instanceof ConstantNode)){ - return new NotOperatorNode({ - right: rightNode, - }); - } - let right = rightNode.value; - let result = !right; - return new ConstantNode({ - value: result, - type: typeof result, - }); - } - toString(){ - let {right} = this; - return `!${right.toString()}`; - } - traverse(fn){ - fn(this); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/OperatorNode.js b/app/imports/parser/parseTree/OperatorNode.js deleted file mode 100644 index 56633993..00000000 --- a/app/imports/parser/parseTree/OperatorNode.js +++ /dev/null @@ -1,71 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class OperatorNode extends ParseNode { - constructor({left, right, operator, fn}) { - super(...arguments); - this.left = left; - this.right = right; - this.fn = fn; - this.operator = operator; - } - resolve(fn, scope, context){ - let leftNode = this.left[fn](scope, context); - let rightNode = this.right[fn](scope, context); - let left, right; - if (!(leftNode instanceof ConstantNode) || !(rightNode instanceof ConstantNode)){ - return new OperatorNode({ - left: leftNode, - right: rightNode, - operator: this.operator, - fn: this.fn, - }); - } else { - left = leftNode.value; - right = rightNode.value; - } - let result; - switch(this.operator){ - case '+': result = left + right; break; - case '-': result = left - right; break; - case '*': result = left * right; break; - case '/': result = left / right; break; - case '^': result = Math.pow(left, right); break; - case '%': result = left % right; break; - case '&': - case '&&': result = left && right; break; - case '|': - case '||': result = left || right; break; - case '=': - case '==': result = left == right; break; - case '===': result = left === right; break; - case '!=': result = left != right; break; - case '!==': result = left !== right; break; - case '>': result = left > right; break; - case '<': result = left < right; break; - case '>=': result = left >= right; break; - case '<=': result = left <= right; break; - } - return new ConstantNode({ - value: result, - type: typeof result, - }); - } - toString(){ - let {left, right, operator} = this; - // special case of adding a negative number - if (operator === '+' && right.isNumber && right.value < 0){ - return `${left.toString()} - ${-right.value}` - } - return `${left.toString()} ${operator} ${right.toString()}`; - } - traverse(fn){ - fn(this); - this.left.traverse(fn); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.left = this.left.replaceNodes(fn); - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/ParenthesisNode.js b/app/imports/parser/parseTree/ParenthesisNode.js deleted file mode 100644 index db6b5e14..00000000 --- a/app/imports/parser/parseTree/ParenthesisNode.js +++ /dev/null @@ -1,30 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; - -export default class ParenthesisNode extends ParseNode { - constructor({content}) { - super(...arguments); - this.content = content; - } - resolve(fn, scope, context){ - let content = this.content[fn](scope, context); - if ( - fn === 'reduce' || - content.constructor.name === 'ConstantNode' || - content.constructor.name === 'ErrorNode' - ){ - return content; - } else { - return new ParenthesisNode({content}); - } - } - toString(){ - return `(${this.content.toString()})`; - } - traverse(fn){ - fn(this); - this.content.traverse(fn); - } - replaceChildren(fn){ - this.content = this.content.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/ParseNode.js b/app/imports/parser/parseTree/ParseNode.js deleted file mode 100644 index 740c7b14..00000000 --- a/app/imports/parser/parseTree/ParseNode.js +++ /dev/null @@ -1,43 +0,0 @@ -export default class ParseNode { - toString(){ - throw new Meteor.Error('toString not implemented on ' + this.constructor.name); - } - compile(scope, context){ - // Returns a ParseNode, a ConstantNode if possible - if(this.resolve) { - return this.resolve('compile', scope, context); - } else { - throw new Meteor.Error('Compile not implemented on ' + this.constructor.name); - } - } - // Compile, but turn rolls into arrays - roll(scope, context){ - if (this.resolve){ - return this.resolve('roll', scope, context); - } else { - return this.compile(scope, context); - } - } - // Compile, turn rolls into arrays, and reduce those arrays into single values - reduce(scope, context){ - if (this.resolve){ - return this.resolve('reduce', scope, context); - } else { - return this.roll(scope, context); - } - } - // If traverse isn't implemented, just apply it to the current node - traverse(fn){ - fn(this); - } - // replace nodes, only replace nodes if fn returns a value - replaceNodes(fn){ - let newNode = fn(this); - if (newNode) { - return newNode; - } else { - if (this.replaceChildren) this.replaceChildren(fn) - return this; - } - } -} diff --git a/app/imports/parser/parseTree/RollArrayNode.js b/app/imports/parser/parseTree/RollArrayNode.js deleted file mode 100644 index 978c20d6..00000000 --- a/app/imports/parser/parseTree/RollArrayNode.js +++ /dev/null @@ -1,22 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class RollArrayNode extends ParseNode { - constructor({values}) { - super(...arguments); - this.values = values; - } - compile(){ - return this; - } - toString(){ - return `[${this.values.join(', ')}]`; - } - reduce(){ - let total = this.values.reduce((a, b) => a + b); - return new ConstantNode({ - value: total, - type: 'number', - }); - } -} diff --git a/app/imports/parser/parseTree/RollNode.js b/app/imports/parser/parseTree/RollNode.js deleted file mode 100644 index 5e1d178f..00000000 --- a/app/imports/parser/parseTree/RollNode.js +++ /dev/null @@ -1,70 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import RollArrayNode from '/imports/parser/parseTree/RollArrayNode.js'; -import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; -import roll from '/imports/parser/roll.js'; -import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; - -export default class RollNode extends ParseNode { - constructor({left, right}) { - super(...arguments); - this.left = left; - this.right = right; - } - compile(scope, context){ - let left = this.left.compile(scope, context); - let right = this.right.compile(scope, context); - return new RollNode({left, right, previousNodes: [this]}); - } - toString(){ - if ( - this.left.isNumberNode && this.left.value === 1 - ){ - return `d${this.right.toString()}`; - } else { - return `${this.left.toString()}d${this.right.toString()}`; - } - } - roll(scope, context){ - let left = this.left.reduce(scope, context); - let right = this.right.reduce(scope, context); - if (!left.isInteger){ - return new ErrorNode({ - node: this, - error: 'Number of dice is not an integer', - }); - } - if (!right.isInteger){ - return new ErrorNode({ - node: this, - error: 'Dice size is not an integer', - }); - } - let number = left.value; - if (context.doubleRolls){ - number *= 2; - } - if (number > STORAGE_LIMITS.diceRollValuesCount) return new ErrorNode({ - node: this, - error: `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`, - context, - }); - let diceSize = right.value; - let values = roll(number, diceSize); - if (context){ - context.storeRoll({number, diceSize, values}); - } - return new RollArrayNode({values}); - } - reduce(scope, context){ - return this.roll(scope, context).reduce(scope, context); - } - traverse(fn){ - fn(this); - this.left.traverse(fn); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.left = this.left.replaceNodes(fn); - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js deleted file mode 100644 index 814ea261..00000000 --- a/app/imports/parser/parseTree/SymbolNode.js +++ /dev/null @@ -1,53 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class SymbolNode extends ParseNode { - constructor({name}){ - super(...arguments); - this.name = name; - } - toString(){ - return `${this.name}` - } - compile(scope, context, calledFromReduce = false){ - let value = scope && scope[this.name]; - let type = typeof value; - // For objects, default to their .value - if (type === 'object'){ - value = value.value; - type = typeof value; - } - // For parse nodes, compile and return - if (value instanceof ParseNode){ - if (calledFromReduce){ - return value.reduce(scope, context); - } else { - return value.compile(scope, context); - } - } - if (type === 'string' || type === 'number' || type === 'boolean'){ - return new ConstantNode({value, type}); - } else if (type === 'undefined'){ - return new SymbolNode({ - name: this.name, - }); - } else { - throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); - } - } - reduce(scope, context){ - let result = this.compile(scope, context, true); - if (result instanceof SymbolNode){ - if (context) context.storeError({ - type: 'info', - message: `${result.toString()} not found, set to 0` - }); - return new ConstantNode({ - type: 'number', - value: 0, - }); - } else { - return result; - } - } -} diff --git a/app/imports/parser/parseTree/UnaryOperatorNode.js b/app/imports/parser/parseTree/UnaryOperatorNode.js deleted file mode 100644 index c8109e6a..00000000 --- a/app/imports/parser/parseTree/UnaryOperatorNode.js +++ /dev/null @@ -1,40 +0,0 @@ -import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; - -export default class UnaryOperatorNode extends ParseNode { - constructor({operator, right}) { - super(...arguments); - this.operator = operator; - this.right = right; - } - resolve(fn, scope, context){ - let rightNode = this.right[fn](scope, context); - if (rightNode.type !== 'number'){ - return new UnaryOperatorNode({ - operator: this.operator, - right: rightNode, - }); - } - let right = rightNode.value; - let result; - switch(this.operator){ - case '-': result = -right; break; - case '+': result = +right; break; - } - return new ConstantNode({ - value: result, - type: typeof result, - }); - } - toString(){ - let {right, operator} = this; - return `${operator}${right.toString()}`; - } - traverse(fn){ - fn(this); - this.right.traverse(fn); - } - replaceChildren(fn){ - this.right = this.right.replaceNodes(fn); - } -} diff --git a/app/imports/parser/parseTree/_index.js b/app/imports/parser/parseTree/_index.js index 571f7e7a..9900c381 100644 --- a/app/imports/parser/parseTree/_index.js +++ b/app/imports/parser/parseTree/_index.js @@ -1,5 +1,31 @@ import accessor from './accessor.js'; +import array from './array.js'; +import call from './call.js'; +import constant from './constant.js'; +import error from './error.js'; +import ifNode from './if.js'; +import index from './index.js'; +import not from './not.js'; +import operator from './operator.js'; +import parenthesis from './parenthesis.js'; +import roll from './roll.js'; +import rollArray from './rollArray.js'; +import symbol from './symbol.js'; +import unaryOperator from './unaryOperator.js'; export default Object.freeze({ accessor, + array, + call, + constant, + error, + if: ifNode, + index, + not, + operator, + parenthesis, + roll, + rollArray, + symbol, + unaryOperator, }); diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.js index 5ebbf246..8c5b3773 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.js @@ -3,7 +3,7 @@ import constant from './constant.js'; const accessor = { create({name, path}) { return { - type: 'accessor', + parseType: 'accessor', path, name, }; @@ -45,12 +45,11 @@ const accessor = { }, reduce(node, scope, context){ let { result } = accessor.compile(node, scope, context); - if (result.type === 'accessor'){ + if (result.parseType === 'accessor'){ context.error(`${accessor.toString(result)} not found, set to 0`); return { result: constant.create({ value: 0, - valueType: 'number', }), context }; diff --git a/app/imports/parser/parseTree/array.js b/app/imports/parser/parseTree/array.js index 407ab92a..7ee711a6 100644 --- a/app/imports/parser/parseTree/array.js +++ b/app/imports/parser/parseTree/array.js @@ -1,10 +1,10 @@ -import constant from './constant.js'; import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; const array = { create({values}) { return { - type: 'array', + parseType: 'array', values, }; }, @@ -24,7 +24,7 @@ const array = { }); return array.create({values}); }, - resolve(fn, node, scope){ + resolve(fn, node, scope, context){ let values = node.values.map(node => { let { result } = resolve(fn, node, scope, context); return result; diff --git a/app/imports/parser/parseTree/call.js b/app/imports/parser/parseTree/call.js index 6893dbf4..52b16967 100644 --- a/app/imports/parser/parseTree/call.js +++ b/app/imports/parser/parseTree/call.js @@ -1,12 +1,12 @@ import error from './error.js'; import constant from './constant.js'; import functions from '/imports/parser/functions.js'; -import resolve, { toString, traverse, mergeResolvedNodes } from '../resolve.js'; +import resolve, { toString, traverse } from '../resolve.js'; const call = { create({functionName, args}) { return { - type: 'call', + parseType: 'call', functionName, args, } @@ -62,7 +62,7 @@ const call = { // Map contant nodes to constants before attempting to run the function let mappedArgs = resolvedArgs.map(arg => { - if (arg.type === 'constant'){ + if (arg.parseType === 'constant'){ return arg.value; } else { return arg; @@ -125,16 +125,10 @@ const call = { } else { type = argumentsExpected[index]; } - if (typeof type === 'string'){ - // Type being a string means a constant node with matching type - if (node.valueType !== type) failed = true; - } else { - // Otherwise check that the node is an instance of the given type - if (!(node instanceof type)) failed = true; - } + if (node.parseType !== type && node.valueType !== type) failed = true; if (failed && fn === 'reduce'){ let typeName = typeof type === 'string' ? type : type.constructor.name; - let nodeName = node.type; + let nodeName = node.parseType; context.error(`Incorrect arguments to ${node.functionName} function` + `expected ${typeName} got ${nodeName}`); } diff --git a/app/imports/parser/parseTree/constant.js b/app/imports/parser/parseTree/constant.js index 4220bc96..e382b897 100644 --- a/app/imports/parser/parseTree/constant.js +++ b/app/imports/parser/parseTree/constant.js @@ -1,14 +1,13 @@ const constant = { - create({value, valueType}){ - if (!valueType) throw `Expected valueType to be set, got ${valueType}` + create({value}){ return { - type: 'constant', - valueType, + parseType: 'constant', + valueType: typeof value, value, } }, - compile(node){ - return node; + compile(node, scope, context){ + return {result: node, context}; }, toString(node){ return `${node.value}`; diff --git a/app/imports/parser/parseTree/error.js b/app/imports/parser/parseTree/error.js index 8533061b..f79ac4af 100644 --- a/app/imports/parser/parseTree/error.js +++ b/app/imports/parser/parseTree/error.js @@ -1,13 +1,13 @@ const error = { create({node, error}) { return { - type: 'error', + parseType: 'error', node, error, } }, - compile(node){ - return node; + compile(node, scope, context){ + return {result: node, context}; }, toString(node){ return node.error.toString(); diff --git a/app/imports/parser/parseTree/if.js b/app/imports/parser/parseTree/if.js index 369ff924..fc56bf66 100644 --- a/app/imports/parser/parseTree/if.js +++ b/app/imports/parser/parseTree/if.js @@ -1,10 +1,9 @@ -import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve'; -import collate from '/imports/api/engine/computation/utility/collate.js'; +import resolve, { traverse, toString } from '../resolve'; const ifNode = { create({condition, consequent, alternative}){ return { - type: 'if', + parseType: 'if', condition, consequent, alternative, @@ -14,28 +13,14 @@ const ifNode = { let {condition, consequent, alternative} = node; return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}` }, - resolve(fn, node, scope){ - let rest, condition, consequent, alternative; - let resolved = {}; + resolve(fn, node, scope, context){ + let {result: condition} = resolve(fn, node.condition, scope, context); - ({result: condition, ...rest} = resolve(fn, node.condition, scope)); - mergeResolvedNodes(resolved, rest); - - if (condition.type === 'constant'){ + if (condition.parseType === 'constant'){ if (condition.value){ - ({result: consequent, ...rest} = resolve(fn, node.consequent, scope)); - mergeResolvedNodes(resolved, rest); - return { - result: consequent, - ...resolved - }; + return resolve(fn, node.consequent, scope, context); } else { - ({result: alternative, ...rest} = resolve(fn, node.alternative, scope)); - mergeResolvedNodes(resolved, rest); - return { - result: alternative, - ...resolved - }; + return resolve(fn, node.alternative, scope, context); } } else { return { @@ -44,7 +29,7 @@ const ifNode = { consequent: node.consequent, alternative: node.alternative, }), - ...resolved + context, }; } }, diff --git a/app/imports/parser/parseTree/index.js b/app/imports/parser/parseTree/index.js index dcb19e2d..17b00a21 100644 --- a/app/imports/parser/parseTree/index.js +++ b/app/imports/parser/parseTree/index.js @@ -1,76 +1,73 @@ -import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve'; +import resolve, { traverse, toString } from '../resolve'; +import error from './error'; const index = { create({array, index}) { return { - type: 'index', + parseType: 'index', array, index, } }, - resolve(fn, node, scope){ - let index, array, rest; - let resolved = {}; - ({result: index, ...rest} = resolve(fn, node.index, scope)); - mergeResolvedNodes(resolved, rest); - ({result: array, ...rest} = resolve(fn, node.array, scope)); - mergeResolvedNodes(resolved, rest); + resolve(fn, node, scope, context){ + let {result: index} = resolve(fn, node.index, scope, context); + let {result: array} = resolve(fn, node.array, scope, context); if ( index.valueType === 'number' && Number.isInteger(index.value) && - array.type === 'array' + array.parseType === 'array' ){ if (index.value < 1 || index.value > array.values.length){ - mergeResolvedNodes(resolved, { - errors: [{ - type: 'warning', - message: `Index of ${index.value} is out of range for an array` + - ` of length ${array.values.length}`, - }] + context.error({ + type: 'warning', + message: `Index of ${index.value} is out of range for an array` + + ` of length ${array.values.length}`, }); } let selection = array.values[index.value - 1]; if (selection){ - let result; - ({result, ...rest} = resolve(fn, selection, scope)); - mergeResolvedNodes(resolved, rest) - return result; + return resolve(fn, selection, scope, context); } } else if (fn === 'reduce'){ - if (!(array instanceof ArrayNode)){ - return new ErrorNode({ - node: node, - error: 'Can not get the index of a non-array node: ' + - node.array.toString() + ' = ' + array.toString(), + if (array.parseType !== 'array'){ + const message = `Can not get the index of a non-array node: ${node.array.toString()} = ${array.toString()}` + context.error(message); + return { + result: error.create({ + node, + error: message, + }), context, - }); + }; } else if (!index.isInteger){ - return new ErrorNode({ - node: node, - error: array.toString() + ' is not an integer index of the array', + const message = `${array.toString()} is not an integer index of the array` + context.error(message); + return { + result: error.create({ + node, + error: message, + }), context, - }); + }; } } - return new IndexNode({ - index, - array, - previousNodes: [node], - }); + return { + result: index.create({ + index, + array, + }), + context, + }; }, - toString(){ - return `${node.array.toString()}[${node.index.toString()}]`; + toString(node){ + return `${toString(node.array)}[${toString(node.index)}]`; }, - traverse(fn){ + traverse(node, fn){ fn(node); - node.array.traverse(fn); - node.index.traverse(fn); + traverse(node.array, fn); + traverse(node.index, fn); }, - replaceChildren(fn){ - node.array = node.array.replaceNodes(fn); - node.index = node.index.replaceNodes(fn); - } } export default index; diff --git a/app/imports/parser/parseTree/not.js b/app/imports/parser/parseTree/not.js new file mode 100644 index 00000000..9488a96c --- /dev/null +++ b/app/imports/parser/parseTree/not.js @@ -0,0 +1,37 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; + +const not = { + create({right}) { + return { + parseType: 'not', + right, + } + }, + resolve(fn, node, scope, context){ + const {result: right} = resolve(fn, node.right, scope, context); + if (right.parseType !== 'constant'){ + return { + result: not.create({ + right: right, + }), + context, + }; + } + return { + result: constant.create({ + value: !right.value, + }), + context, + }; + }, + toString(node){ + return `!${toString(node.right)}`; + }, + traverse(node, fn){ + fn(node); + traverse(node.right, fn); + } +} + +export default not; diff --git a/app/imports/parser/parseTree/operator.js b/app/imports/parser/parseTree/operator.js new file mode 100644 index 00000000..7cc4a099 --- /dev/null +++ b/app/imports/parser/parseTree/operator.js @@ -0,0 +1,76 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; + +const operator = { + create({left, right, operator, fn}) { + return { + parseType: 'operator', + left, + right, + operator, + fn + }; + }, + resolve(fn, node, scope, context){ + const {result: leftNode} = resolve(fn, node.left, scope, context); + const {result: rightNode} = resolve(fn, node.right, scope, context); + let left, right; + if (leftNode.parseType !== 'constant' || rightNode.parseType !== 'constant'){ + return { + result: operator.create({ + left: leftNode, + right: rightNode, + operator: node.operator, + fn: node.fn, + }), + context, + }; + } else { + left = leftNode.value; + right = rightNode.value; + } + let result; + switch(node.operator){ + case '+': result = left + right; break; + case '-': result = left - right; break; + case '*': result = left * right; break; + case '/': result = left / right; break; + case '^': result = Math.pow(left, right); break; + case '%': result = left % right; break; + case '&': + case '&&': result = left && right; break; + case '|': + case '||': result = left || right; break; + case '=': + case '==': result = left == right; break; + case '===': result = left === right; break; + case '!=': result = left != right; break; + case '!==': result = left !== right; break; + case '>': result = left > right; break; + case '<': result = left < right; break; + case '>=': result = left >= right; break; + case '<=': result = left <= right; break; + } + return { + result: constant.create({ + value: result, + }), + context, + }; + }, + toString(node){ + let {left, right, operator} = node; + // special case of adding a negative number + if (operator === '+' && right.valueType === 'number' && right.value < 0){ + return `${toString(left)} - ${-right.value}` + } + return `${toString(left)} ${operator} ${toString(right)}`; + }, + traverse(node, fn){ + fn(node); + traverse(node.left, fn); + traverse(node.right, fn); + }, +} + +export default operator; diff --git a/app/imports/parser/parseTree/parenthesis.js b/app/imports/parser/parseTree/parenthesis.js new file mode 100644 index 00000000..cd0cfa1a --- /dev/null +++ b/app/imports/parser/parseTree/parenthesis.js @@ -0,0 +1,34 @@ +import resolve, { toString, traverse } from '../resolve.js'; + +const parenthesis = { + create({content}) { + return { + parseType: 'parenthesis', + content, + }; + }, + resolve(fn, node, scope, context){ + const {result: content} = resolve(fn, node.content, scope, context); + if ( + fn === 'reduce' || + content.parseType === 'constant' || + content.parseType === 'error' + ){ + return {result: content, context}; + } else { + return { + result: parenthesis.create({content}), + context + }; + } + }, + toString(node){ + return `(${toString(node.content)})`; + }, + traverse(node, fn){ + fn(node); + traverse(node.content, fn); + } +} + +export default parenthesis; diff --git a/app/imports/parser/parseTree/roll.js b/app/imports/parser/parseTree/roll.js new file mode 100644 index 00000000..cc87cbcc --- /dev/null +++ b/app/imports/parser/parseTree/roll.js @@ -0,0 +1,82 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import error from './error.js'; +import rollArray from './rollArray.js'; +import roll from '/imports/parser/roll.js'; +import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS.js'; + +const rollNode = { + create({left, right}) { + return { + parseType: 'roll', + left, + right, + }; + }, + compile(node, scope, context){ + const {result: left} = resolve('compile', node.left, scope, context); + const {result: right} = resolve('compile', node.right, scope, context); + return { + result: rollNode.create({left, right}), + context, + }; + }, + toString(node){ + if ( + node.left.parseType === 'number' && node.left.value === 1 + ){ + return `d${toString(node.right)}`; + } else { + return `${toString(node.left)}d${toString(node.right)}`; + } + }, + roll(node, scope, context){ + const {result: left} = resolve('reduce', node.left, scope, context); + const {result: right} = resolve('reduce', node.right, scope, context); + if (left.parseType !== 'number' && !Number.isInteger(left.value)){ + return errorResult('Number of dice is not an integer', node, context); + } + if (!right.isInteger){ + return errorResult('Dice size is not an integer', node, context); + } + let number = left.value; + if (context.doubleRolls){ + number *= 2; + } + if (number > STORAGE_LIMITS.diceRollValuesCount){ + const message = `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`; + return errorResult(message, node, context); + } + let diceSize = right.value; + let values = roll(number, diceSize); + if (context){ + context.storeRoll({number, diceSize, values}); + } + return { + result: rollArray.create({ + values, + diceSize, + diceNum: left.value, + }), + context + }; + }, + reduce(node, scope, context){ + const {result} = rollNode.roll(node, scope, context); + return resolve('reduce', result, scope, context); + }, + traverse(node, fn){ + fn(node); + traverse(node.left, fn); + traverse(node.right, fn); + }, +} + +function errorResult(message, node, context){ + context.error(message); + return { + result: error.create({ node, error: message }), + context, + }; +} + +export default rollNode; diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js new file mode 100644 index 00000000..bb778947 --- /dev/null +++ b/app/imports/parser/parseTree/rollArray.js @@ -0,0 +1,35 @@ +import constant from './constant.js'; + +const rollArray = { + constructor({values, diceSize, diceNum}) { + return { + parseType: 'rollArray', + values, + diceSize, + diceNum, + }; + }, + compile(node, scope, context){ + return { + result: node, + context + }; + }, + toString(node){ + return `[${node.values.join(', ')}]`; + }, + reduce(node, scope, context){ + const total = node.values.reduce((a, b) => a + b, 0); + return { + result: constant.create({ + value: total, + }), + context, + }; + }, + traverse(node, fn){ + return fn(node); + } +} + +export default rollArray; diff --git a/app/imports/parser/parseTree/symbol.js b/app/imports/parser/parseTree/symbol.js new file mode 100644 index 00000000..208baa66 --- /dev/null +++ b/app/imports/parser/parseTree/symbol.js @@ -0,0 +1,61 @@ +import resolve from '../resolve.js'; +import constant from './constant.js'; + +const symbol = { + create({name}){ + return { + parseType: 'symbol', + name, + }; + }, + toString(node){ + return `${node.name}` + }, + compile(node, scope, context, calledFromReduce = false){ + let value = scope && scope[node.name]; + let type = typeof value; + // For objects, default to their .value + if (type === 'object'){ + value = value.value; + type = typeof value; + } + // For parse nodes, compile and return + if (value?.parseType){ + if (calledFromReduce){ + return resolve('reduce', value, scope, context); + } else { + return resolve('compile', value, scope, context); + } + } + if (type === 'string' || type === 'number' || type === 'boolean'){ + return { + result: constant.create({value}), + context, + }; + } else if (type === 'undefined'){ + return { + result: symbol.create({name: node.name}), + context, + }; + } else { + throw new Meteor.Error(`Unexpected case: ${node.name} resolved to ${value}`); + } + }, + reduce(node, scope, context){ + let {result} = symbol.compile(node, scope, context, true); + if (result.parseType === 'symbol'){ + context.error({ + type: 'info', + message: `${result.toString()} not found, set to 0` + }); + return { + result: constant.create({value: 0}), + context, + }; + } else { + return {result, context}; + } + } +} + +export default symbol; diff --git a/app/imports/parser/parseTree/unaryOperator.js b/app/imports/parser/parseTree/unaryOperator.js new file mode 100644 index 00000000..1b348f90 --- /dev/null +++ b/app/imports/parser/parseTree/unaryOperator.js @@ -0,0 +1,46 @@ +import resolve, { toString, traverse } from '../resolve.js'; +import constant from './constant.js'; + +const unaryOperator = { + create({operator, right}) { + return { + parseType: 'unaryOperator', + operator, + right, + }; + }, + resolve(fn, node, scope, context){ + const {result: rightNode} = resolve(fn, node.right, scope, context); + if (rightNode.parseType !== 'number'){ + return { + result: unaryOperator.create({ + operator: node.operator, + right: rightNode, + }), + context, + }; + } + let right = rightNode.value; + let result; + switch(node.operator){ + case '-': result = -right; break; + case '+': result = +right; break; + } + return { + result: constant.create({ + value: result, + parseType: typeof result, + }), + context, + }; + }, + toString(node){ + return `${node.operator}${toString(node.right)}`; + }, + traverse(node, fn){ + fn(node); + traverse(node.right, fn); + }, +}; + +export default unaryOperator; diff --git a/app/imports/parser/parser.js b/app/imports/parser/parser.js index 13f24579..8532055b 100644 --- a/app/imports/parser/parser.js +++ b/app/imports/parser/parser.js @@ -7,20 +7,6 @@ export default function parser(){ return new nearley.Parser(nearleyGrammar); } -export class CompilationContext { - constructor({doubleRolls} = {}){ - this.errors = []; - this.rolls = []; - this.doubleRolls = doubleRolls; - } - storeError(e){ - this.errors.push(e); - } - storeRoll(r){ - this.rolls.push(r); - } -} - export function parse(string){ let parser = new nearley.Parser(nearleyGrammar); parser.feed(string); diff --git a/app/imports/parser/resolve.js b/app/imports/parser/resolve.js index 86d2fad6..add29f25 100644 --- a/app/imports/parser/resolve.js +++ b/app/imports/parser/resolve.js @@ -1,13 +1,11 @@ -import nodeTypeIndex from './parseTree/index.js'; -import collate from '/imports/api/engine/computation/utility/collate.js'; -import Context from './ResolveContext.js'; +import nodeTypeIndex from './parseTree/_index.js'; // Takes a parse ndoe and computes it to a set detail level // returns {result, context} export default function resolve(fn, node, scope, context = new Context()){ - let type = nodeTypeIndex[node.type]; + let type = nodeTypeIndex[node.parseType]; if (!type){ - throw new Meteor.Error(`Parse node type: ${node.type} not implemented`); + throw new Meteor.Error(`Parse node type: ${node.parseType} not implemented`); } if (type.resolve){ return type.resolve(fn, node, scope, context); @@ -18,28 +16,49 @@ export default function resolve(fn, node, scope, context = new Context()){ } else if (type.compile){ return type.compile(node, scope, context) } else { - throw new Meteor.Error('Compile not implemented on ' + node.type); + throw new Meteor.Error('Compile not implemented on ' + node.parseType); } } export function toString(node){ - let type = nodeTypeIndex[node.type]; + let type = nodeTypeIndex[node.parseType]; if (!type.toString){ - throw new Meteor.Error('toString not implemented on ' + node.type); + throw new Meteor.Error('toString not implemented on ' + node.parseType); } return type.toString(node); } export function traverse(node, fn){ - let type = nodeTypeIndex[node.type]; + if (!node) return; + let type = nodeTypeIndex[node.parseType]; + if (!type){ + console.error(node); + throw new Meteor.Error('Not valid parse node'); + } if (type.traverse){ return type.traverse(node, fn); } return fn(node); } -export function mergeResolvedNodes(main, other){ - main.errors = collate(main.errors, other.errors); - main.rolls = collate(main.rolls, other.rolls); - return main; +export class Context { + constructor({errors = [], rolls = [], doubleRolls} = {}){ + this.errors = errors; + this.rolls = rolls; + this.doubleRolls = doubleRolls; + } + error(e){ + if (!e) return; + if (typeof e === 'string'){ + this.errors.push({ + type: 'error', + message: e, + }); + } else { + this.errors.push(e); + } + } + roll(r){ + this.rolls.push(r); + } }