diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index c158d3ea..310a2ad8 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -79,7 +79,7 @@ let ParserRules = [ {"name": "exponentExpression", "symbols": ["callExpression", "_", (lexer.has("exponentOperator") ? {type: "exponentOperator"} : exponentOperator), "_", "exponentExpression"], "postprocess": d => operator(d, 'exponent')}, {"name": "exponentExpression", "symbols": ["callExpression"], "postprocess": id}, {"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess": - d => new CallNode ({type: "call", fn: d[0], arguments: d[2]}) + d => new CallNode ({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]}, diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index 3f6c7fc6..4e199c11 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -103,7 +103,7 @@ exponentExpression -> callExpression -> name _ arguments {% - d => new CallNode ({type: "call", fn: d[0], arguments: d[2]}) + d => new CallNode ({functionName: d[0].name, args: d[2]}) %} | indexExpression {% id %} diff --git a/app/imports/parser/parseTree/ArrayNode.js b/app/imports/parser/parseTree/ArrayNode.js index 43f592ad..b74fc455 100644 --- a/app/imports/parser/parseTree/ArrayNode.js +++ b/app/imports/parser/parseTree/ArrayNode.js @@ -2,12 +2,12 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; export default class ArrayNode extends ParseNode { constructor({values}) { - super(); + super(...arguments); this.values = values; } - compile(){ - let values = this.values.map(node => node.compile()); - return new ArrayNode({values}); + compile(scope){ + let values = this.values.map(node => node.compile(scope)); + return new ArrayNode({values, previousNodes: [this]}); } toString(){ return `[${this.values.map(node => node.toString()).join(', ')}]`; diff --git a/app/imports/parser/parseTree/CallNode.js b/app/imports/parser/parseTree/CallNode.js index 8ad1dfb5..52ab227c 100644 --- a/app/imports/parser/parseTree/CallNode.js +++ b/app/imports/parser/parseTree/CallNode.js @@ -1 +1,82 @@ -//TODO +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; +import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; +import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; + +export default class CallNode extends ParseNode { + constructor({functionName, args}) { + super(...arguments); + this.functionName = functionName; + this.args = args; + } + compile(scope){ + return this.resolve('compile', scope); + } + roll(scope){ + return this.resolve('roll', scope); + } + reduce(scope){ + return this.resolve('reduce', scope); + } + resolve(fn, scope){ + let func = functions[this.functionName]; + if (!func) return new ErrorNode({ + node: this, + error: `${this.functionName} is not a function`, + previousNodes: [this], + }); + let args = castArgsToType({fn, scope, args: this.args, type: func.argumentType}); + if (args.failed){ + if (fn === 'reduce'){ + return new ErrorNode({ + node: this, + error: 'Could not convert all arguments to the correct type', + previousNodes: [this], + }); + } else { + return new CallNode({ + functionName: this.functionName, + args: args, + previousNodes: [this], + }); + } + } else { + let value = func.fn.apply(null, args); + console.log({args}) + return new ConstantNode({ + value, + type: 'number', + previousNodes: [this], + }); + } + + } + toString(){ + return `${this.functionName}(${this.args.map(node => node.toString()).join(', ')})`; + } +} + +const functions = { + 'min': { + comment: 'Returns the lowest of the given numbers', + argumentType: 'number', + resultType: 'number', + fn: Math.min, + }, +} + +function castArgsToType({fn, scope, args, type}){ + let resolvedArgs = args.map(node => node[fn](scope)) + let result = []; + if (type === 'number'){ + resolvedArgs.forEach(node => { + if (node.isNumber){ + result.push(node.value); + } else { + resolvedArgs.failed = true; + } + }) + } + if (resolvedArgs.failed) return resolvedArgs; + console.log({result}) + return result; +} diff --git a/app/imports/parser/parseTree/ConstantNode.js b/app/imports/parser/parseTree/ConstantNode.js index 4d3198fd..268011eb 100644 --- a/app/imports/parser/parseTree/ConstantNode.js +++ b/app/imports/parser/parseTree/ConstantNode.js @@ -1,12 +1,12 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; export default class ConstantNode extends ParseNode { - constructor({value, type, errors}){ - super(); + constructor({value, type, rollTable}){ + super(...arguments); // string, number, boolean, numberArray, uncompiledNode this.type = type; this.value = value; - if (errors) this.errors = errors; + if (rollTable) this.rollTable = rollTable; } compile(){ return this; @@ -18,6 +18,6 @@ export default class ConstantNode extends ParseNode { return this.type === 'number'; } get isInteger(){ - return this.isNumberNode && Number.isInteger(this.value); + return this.type === 'number' && Number.isInteger(this.value); } } diff --git a/app/imports/parser/parseTree/ErrorNode.js b/app/imports/parser/parseTree/ErrorNode.js index bbef94c3..44fe2434 100644 --- a/app/imports/parser/parseTree/ErrorNode.js +++ b/app/imports/parser/parseTree/ErrorNode.js @@ -2,7 +2,7 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; export default class ErrorNode extends ParseNode { constructor({node, error}) { - super(); + super(...arguments); this.node = node; this.error = error; } diff --git a/app/imports/parser/parseTree/IfNode.js b/app/imports/parser/parseTree/IfNode.js index a801e4bd..66334d84 100644 --- a/app/imports/parser/parseTree/IfNode.js +++ b/app/imports/parser/parseTree/IfNode.js @@ -1,9 +1,8 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; export default class IfNode extends ParseNode { constructor({condition, consequent, alternative}){ - super(); + super(...arguments); this.condition = condition; this.consequent = consequent; this.alternative = alternative; @@ -12,33 +11,26 @@ export default class IfNode extends ParseNode { let {condition, consequent, alternative} = this; return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}` } - compile(){ - let condition = this.condition.compile(); - let consequent = this.consequent.compile(); - let alternative = this.alternative.compile(); - if ( - condition.type !== 'string' && - condition.type !== 'number' && - condition.type !== 'boolean' - ){ - // Handle unresolved condition - return new ConstantNode({ - value: `${condition.value}) ${consequent.value} else ${alternative.value}`, - type: 'uncompiledNode', - errors: [ - ...condition.errors, - ...consequent.errors, - ...alternative.errors, - ], - }); + compile(scope){ + return this.resolve('compile', scope); + } + roll(scope){ + return this.resolve('roll', scope); + } + reduce(scope){ + return this.resolve('reduce', scope); + } + resolve(fn, scope){ + let condition = this.condition[fn](scope); + let consequent = this.consequent[fn](scope); + let alternative = this.alternative[fn](scope); + this.resolve(condition, consequent, alternative); + if (condition.value){ + consequent.inheritDetails([condition, this]); + return consequent; } else { - // So long as the condition reolves, return the correct alternative, - // even if it's unresolved - if (condition.value){ - return consequent; - } else { - return alternative; - } + alternative.inheritDetails([condition, this]); + return alternative; } } } diff --git a/app/imports/parser/parseTree/IndexNode.js b/app/imports/parser/parseTree/IndexNode.js index 158da8c6..8014d942 100644 --- a/app/imports/parser/parseTree/IndexNode.js +++ b/app/imports/parser/parseTree/IndexNode.js @@ -2,23 +2,35 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; export default class IndexNode extends ParseNode { constructor({array, index}) { - super(); + super(...arguments); this.array = array; this.index = index; } - compile(){ - let index = this.index.compile(); - if (index.constructor.name === 'ConstantNode' && index.type === 'number'){ - let selection = this.array.values[index.value]; + resolve(fn, scope){ + let index = this.index[fn](scope); + if (index.isInteger){ + let selection = this.array.values[index.value - 1]; if (selection){ - return selection.compile(); + let result = selection[fn](scope); + result.inheritDetails([index, this]); + return result; } } return new IndexNode({ - array: this.array.compile(), - index: this.index.compile(), + array: this.array[fn](scope), + index: this.index[fn](scope), + previousNodes: [this], }); } + compile(scope){ + return this.resolve('compile', scope); + } + roll(scope){ + return this.resolve('roll', scope); + } + reduce(scope){ + return this.resolve('reduce', scope); + } toString(){ return `${this.array.toString()}[${this.index.toString()}]`; } diff --git a/app/imports/parser/parseTree/OperatorNode.js b/app/imports/parser/parseTree/OperatorNode.js index fa75f315..5b6830d8 100644 --- a/app/imports/parser/parseTree/OperatorNode.js +++ b/app/imports/parser/parseTree/OperatorNode.js @@ -3,22 +3,32 @@ import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; export default class OperatorNode extends ParseNode { constructor({left, right, operator, fn}) { - super(); + super(...arguments); this.left = left; this.right = right; this.fn = fn; this.operator = operator; } - compile(){ - let leftNode = this.left.compile(); - let rightNode = this.right.compile(); + compile(scope){ + return this.resolve('compile', scope); + } + roll(scope){ + return this.resolve('roll', scope); + } + reduce(scope){ + return this.resolve('reduce', scope); + } + resolve(fn, scope){ + let leftNode = this.left[fn](scope); + let rightNode = this.right[fn](scope); let left, right; if (leftNode.type !== 'number' || rightNode.type !== 'number'){ return new OperatorNode({ left: leftNode, right: rightNode, operator: this.operator, - fn: this.fn + fn: this.fn, + previousNodes: [this], }); } else { left = leftNode.value; @@ -44,7 +54,11 @@ export default class OperatorNode extends ParseNode { case '>=': result = left >= right; break; case '<=': result = left <= right; break; } - return new ConstantNode({value: result, type: typeof result}); + return new ConstantNode({ + value: result, + type: typeof result, + previousNodes: [this, leftNode, rightNode], + }); } toString(){ let {left, right, operator} = this; diff --git a/app/imports/parser/parseTree/ParenthesisNode.js b/app/imports/parser/parseTree/ParenthesisNode.js index 22da9c4b..2df81c3e 100644 --- a/app/imports/parser/parseTree/ParenthesisNode.js +++ b/app/imports/parser/parseTree/ParenthesisNode.js @@ -2,15 +2,24 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; export default class ParenthesisNode extends ParseNode { constructor({content}) { - super(); + super(...arguments); this.content = content; } - compile(){ - let content = this.content.compile(); + compile(scope){ + return this.resolve('compile', scope); + } + roll(scope){ + return this.resolve('roll', scope); + } + reduce(scope){ + return this.resolve('reduce', scope); + } + resolve(fn, scope){ + let content = this.content[fn](scope); if (content.constructor.name === 'ConstantNode'){ return content; } else { - return new ParenthesisNode({content}); + return new ParenthesisNode({content, previousNodes: [this]}); } } toString(){ diff --git a/app/imports/parser/parseTree/ParseNode.js b/app/imports/parser/parseTree/ParseNode.js index 0d8686d3..03f054ac 100644 --- a/app/imports/parser/parseTree/ParseNode.js +++ b/app/imports/parser/parseTree/ParseNode.js @@ -1,4 +1,17 @@ export default class ParseNode { + constructor({previousNodes, detail}){ + this.inheritDetails(previousNodes); + if (detail) this.pushDetails([detail]); + } + inheritDetails(nodes){ + if (!nodes || !nodes.length) return; + nodes.forEach(node => this.pushDetails(node.details)); + } + pushDetails(details){ + if (!details || !details.length) return; + if (!this.details) this.details = []; + details.forEach(detail => this.details.push(detail)); + } compile(){ // Returns a ParseNode, a ConstantNode if possible throw new Meteor.Error('Compile not implemented on ' + this.constructor.name); @@ -7,11 +20,11 @@ export default class ParseNode { throw new Meteor.Error('toString not implemented on ' + this.constructor.name); } // Compile, but turn rolls into arrays - roll(){ - return this.compile(); + roll(scope){ + return this.compile(scope); } // Compile, turn rolls into arrays, and reduce those arrays into single values - reduce(){ - return this.roll(); + reduce(scope){ + return this.roll(scope); } } diff --git a/app/imports/parser/parseTree/RollArrayNode.js b/app/imports/parser/parseTree/RollArrayNode.js index d8d60236..fe21abd3 100644 --- a/app/imports/parser/parseTree/RollArrayNode.js +++ b/app/imports/parser/parseTree/RollArrayNode.js @@ -1,8 +1,9 @@ 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(); + super(...arguments); this.values = values; } compile(){ @@ -12,6 +13,11 @@ export default class RollArrayNode extends ParseNode { return `[${this.values.join(', ')}]`; } reduce(){ - //TODO sum and return values + let total = this.values.reduce((a, b) => a + b); + return new ConstantNode({ + value: total, + type: 'number', + previousNodes: [this], + }); } } diff --git a/app/imports/parser/parseTree/RollNode.js b/app/imports/parser/parseTree/RollNode.js index d2f99379..8f141832 100644 --- a/app/imports/parser/parseTree/RollNode.js +++ b/app/imports/parser/parseTree/RollNode.js @@ -1,17 +1,17 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; -import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; +import RollArrayNode from '/imports/parser/parseTree/RollArrayNode.js'; import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; export default class RollNode extends ParseNode { constructor({left, right}) { - super(); + super(...arguments); this.left = left; this.right = right; } - compile(){ - let left = this.left.compile(); - let right = this.right.compile(); - return new RollNode({left, right}); + compile(scope){ + let left = this.left.compile(scope); + let right = this.right.compile(scope); + return new RollNode({left, right, previousNodes: [this]}); } toString(){ if ( @@ -22,19 +22,21 @@ export default class RollNode extends ParseNode { return `${this.left.toString()}d${this.right.toString()}`; } } - roll(){ - let left = this.left.reduce(); - let right = this.right.reduce(); + roll(scope){ + let left = this.left.reduce(scope); + let right = this.right.reduce(scope); if (!left.isInteger){ return new ErrorNode({ node: this, - error: 'Number of dice is not an integer' + error: 'Number of dice is not an integer', + previousNodes: [this, left, right], }); } if (!right.isInteger){ return new ErrorNode({ node: this, - error: 'Dice size is not an integer' + error: 'Dice size is not an integer', + previousNodes: [this, left, right], }); } let number = left.value; @@ -44,14 +46,18 @@ export default class RollNode extends ParseNode { }); let diceSize = right.value; let randomSrc = DDP.randomStream('diceRoller'); - let rolls = []; + let values = []; for (let i = 0; i < number; i++){ let roll = ~~(randomSrc.fraction() * diceSize) + 1 - rolls.push(roll); + values.push(roll); } - return new ArrayNode({values: rolls}); + return new RollArrayNode({ + values, + detail: {number, diceSize, values}, + previousNodes: [this, left, right], + }); } - reduce(){ - this.roll().reduce(); + reduce(scope){ + return this.roll(scope).reduce(scope); } } diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js index cb855b72..35c2ffab 100644 --- a/app/imports/parser/parseTree/SymbolNode.js +++ b/app/imports/parser/parseTree/SymbolNode.js @@ -1,9 +1,10 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; +import ErrorNode from '/imports/parser/parseTree/ErrorNode.js'; export default class SymbolNode extends ParseNode { constructor({name}){ - super(); + super(...arguments); this.name = name; } toString(){ @@ -13,12 +14,11 @@ export default class SymbolNode extends ParseNode { let value = scope && scope[this.name]; let type = typeof value; if (type === 'string' || type === 'number' || type === 'boolean'){ - return new ConstantNode({value, type}); + return new ConstantNode({value, type, previousNodes: [this]}); } else if (type === 'undefined'){ - return new ConstantNode({ - value: this.name, - type: 'uncompiledNode', - errors: [`${this.name} could not be resolved`] + return new ErrorNode({ + node: this, + error: `${this.name} could not be resolved`, }); } else { throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); diff --git a/app/imports/ui/pages/Parser.vue b/app/imports/ui/pages/Parser.vue index abcc4e4b..ba8d7683 100644 --- a/app/imports/ui/pages/Parser.vue +++ b/app/imports/ui/pages/Parser.vue @@ -18,6 +18,21 @@ readonly label="compiled" /> + + + @@ -32,6 +47,9 @@ export default { output: null, compiled: null, string: null, + rolled: null, + reduced: null, + reducedJson: null, }}, watch: { input(val){ @@ -41,7 +59,11 @@ export default { this.output = JSON.stringify(output, null, 2); if (!output) return; this.string = output; - this.compiled = output.compile(); + let scope = {cat: 1, dog: 2, mouse: 3}; + this.compiled = output.compile(scope); + this.rolled = output.roll(scope); + this.reduced = output.reduce(scope); + this.reducedJson = JSON.stringify(this.reduced, null, 2) } }, } diff --git a/app/package-lock.json b/app/package-lock.json index 711f73f9..8f43e02d 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -2805,7 +2805,7 @@ }, "signal-exit": { "version": "3.0.2", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", + "resolved": false, "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" }, "simpl-schema": {