From b7b0ac9c000c942fb0fe9871ba473b4f5c208c12 Mon Sep 17 00:00:00 2001 From: Thaum Rystra Date: Sat, 6 Apr 2019 10:56:53 +0200 Subject: [PATCH] Separated parser class nodes and began writing compile methods --- .../api/creature/creatureComputation.js | 2 + app/imports/parser/compileFunctions/index.js | 7 ++ app/imports/parser/compileFunctions/sum.js | 15 +++ app/imports/parser/grammar.js | 59 ++++-------- app/imports/parser/grammar.ne | 16 +++- app/imports/parser/parseTree.js | 96 ------------------- app/imports/parser/parseTree/CallNode.js | 1 + app/imports/parser/parseTree/ConstantNode.js | 21 ++++ app/imports/parser/parseTree/IfNode.js | 40 ++++++++ app/imports/parser/parseTree/OperatorNode.js | 11 +++ app/imports/parser/parseTree/ParseNode.js | 14 +++ app/imports/parser/parseTree/RollNode.js | 0 app/imports/parser/parseTree/SymbolNode.js | 24 +++++ app/package-lock.json | 68 +------------ 14 files changed, 167 insertions(+), 207 deletions(-) create mode 100644 app/imports/parser/compileFunctions/index.js create mode 100644 app/imports/parser/compileFunctions/sum.js delete mode 100644 app/imports/parser/parseTree.js create mode 100644 app/imports/parser/parseTree/CallNode.js create mode 100644 app/imports/parser/parseTree/ConstantNode.js create mode 100644 app/imports/parser/parseTree/IfNode.js create mode 100644 app/imports/parser/parseTree/OperatorNode.js create mode 100644 app/imports/parser/parseTree/ParseNode.js create mode 100644 app/imports/parser/parseTree/RollNode.js create mode 100644 app/imports/parser/parseTree/SymbolNode.js diff --git a/app/imports/api/creature/creatureComputation.js b/app/imports/api/creature/creatureComputation.js index 7029174d..90fe7c60 100644 --- a/app/imports/api/creature/creatureComputation.js +++ b/app/imports/api/creature/creatureComputation.js @@ -12,6 +12,8 @@ import Proficiencies from "/imports/api/creature/properties/Proficiencies.js"; import DamageMultipliers from "/imports/api/creature/properties/DamageMultipliers.js"; import Classes from "/imports/api/creature/properties/Classes.js"; import * as math from 'mathjs'; +import parser from '/imports/parser/parser.js'; +if (Meteor.isClient) console.log({parser}); export const recomputeCreature = new ValidatedMethod({ diff --git a/app/imports/parser/compileFunctions/index.js b/app/imports/parser/compileFunctions/index.js new file mode 100644 index 00000000..3fe104cc --- /dev/null +++ b/app/imports/parser/compileFunctions/index.js @@ -0,0 +1,7 @@ +// All of the compile functions are provided for use in compiling parse trees +// Every compile function takes in ParseNodes as arguements and returns a single +// ConstantNode as a result + +const compileFunctions = {}; + +export compileFunctions; diff --git a/app/imports/parser/compileFunctions/sum.js b/app/imports/parser/compileFunctions/sum.js new file mode 100644 index 00000000..32f78e70 --- /dev/null +++ b/app/imports/parser/compileFunctions/sum.js @@ -0,0 +1,15 @@ +export default function sum(inputNode) { + let node = inputNode.roll(); + if (node.type === 'numberArray'){ + let total = node.value.reduce((total, num) => total + num, 0); + return new ConstantNode({type: 'number', value: total}); + } else { + let errors = node.errors || []; + errors.push(`Could not sum ${node.value}`); + return new ConstantNode({ + type: 'uncompiledNode', + value: node.value, + errors, + }); + } +} diff --git a/app/imports/parser/grammar.js b/app/imports/parser/grammar.js index ea992829..7ab4c9ed 100644 --- a/app/imports/parser/grammar.js +++ b/app/imports/parser/grammar.js @@ -1,9 +1,13 @@ // Generated automatically by nearley, version 2.16.0 // http://github.com/Hardmath123/nearley -(function () { function id(x) { return x[0]; } - const moo = require("moo"); + 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 OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; + import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; + import moo from 'moo'; const lexer = moo.compile({ number: /[0-9]+(?:\.[0-9]+)?/, @@ -35,15 +39,6 @@ function id(x) { return x[0]; } }); function nuller() { return null; } - - class OperatorNode { - constructor({left, right, operator, fn}) { - this.left = left; - this.right = right; - this.fn = fn; - this.operator = operator; - } - } function operator([left, _1, operator, _2, right], fn){ return new OperatorNode({ left, @@ -52,23 +47,11 @@ function id(x) { return x[0]; } fn }); } - - class SymbolNode { - constructor(name){ - this.name = name; - } - } - - class ConstantNode { - constructor(value, type){ - this.type = type; - this.value = value; - } - } -var grammar = { - Lexer: lexer, - ParserRules: [ - {"name": "ifStatement", "symbols": [{"literal":"if"}, "_", {"literal":"("}, "_", "expression", "_", {"literal":")"}, "_", "ifStatement", "_", {"literal":"else"}, "_", "ifStatement"], "postprocess": d => ({condition: d[4], true: d[8], false: d[12]})}, +let Lexer = lexer; +let ParserRules = [ + {"name": "ifStatement", "symbols": [{"literal":"if"}, "_", {"literal":"("}, "_", "expression", "_", {"literal":")"}, "_", "ifStatement", "_", {"literal":"else"}, "_", "ifStatement"], "postprocess": + d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]}) + }, {"name": "ifStatement", "symbols": ["expression"], "postprocess": id}, {"name": "expression", "symbols": ["equalityExpression"], "postprocess": d => d[0]}, {"name": "equalityExpression", "symbols": ["equalityExpression", "_", (lexer.has("equalityOperator") ? {type: "equalityOperator"} : equalityOperator), "_", "relationalExpression"], "postprocess": d => operator(d, 'equality')}, @@ -88,7 +71,7 @@ var grammar = { {"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 => ({type: "call", function: d[0], arguments: d[2]}) + d => new CallNode ({type: "call", fn: d[0], arguments: d[2]}) }, {"name": "callExpression", "symbols": ["parenthesizedExpression"], "postprocess": id}, {"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, @@ -105,17 +88,11 @@ var grammar = { {"name": "valueExpression", "symbols": ["name"], "postprocess": id}, {"name": "valueExpression", "symbols": ["number"], "postprocess": id}, {"name": "valueExpression", "symbols": ["string"], "postprocess": id}, - {"name": "number", "symbols": [(lexer.has("number") ? {type: "number"} : number)], "postprocess": d => new ConstantNode(d[0].value, 'number')}, - {"name": "name", "symbols": [(lexer.has("name") ? {type: "name"} : name)], "postprocess": d => new SymbolNode(d[0].value)}, - {"name": "string", "symbols": [(lexer.has("string") ? {type: "string"} : string)], "postprocess": d => new ConstantNode(d[0].value, 'string')}, + {"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": "_", "symbols": []}, {"name": "_", "symbols": [(lexer.has("space") ? {type: "space"} : space)], "postprocess": nuller} -] - , ParserStart: "ifStatement" -} -if (typeof module !== 'undefined'&& typeof module.exports !== 'undefined') { - module.exports = grammar; -} else { - window.grammar = grammar; -} -})(); +]; +let ParserStart = "ifStatement"; +export default { Lexer, ParserRules, ParserStart }; diff --git a/app/imports/parser/grammar.ne b/app/imports/parser/grammar.ne index b50254f9..22563eb0 100644 --- a/app/imports/parser/grammar.ne +++ b/app/imports/parser/grammar.ne @@ -1,5 +1,11 @@ +@preprocessor esmodule @{% - const moo = require("moo"); + 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 OperatorNode from '/imports/parser/parseTree/OperatorNode.js'; + import SymbolNode from '/imports/parser/parseTree/SymbolNode.js'; + import moo from 'moo'; const lexer = moo.compile({ number: /[0-9]+(?:\.[0-9]+)?/, @@ -46,7 +52,7 @@ ifStatement -> "if" _ "(" _ expression _ ")" _ ifStatement _ "else" _ ifStatement {% - d => new ifNode({condition: d[4], consequent: d[8], alternative: d[12]}) + d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]}) %} | expression {% id %} @@ -87,7 +93,7 @@ exponentExpression -> callExpression -> name _ arguments {% - d => ({type: "call", function: d[0], arguments: d[2]}) + d => new CallNode ({type: "call", fn: d[0], arguments: d[2]}) %} | parenthesizedExpression {% id %} @@ -107,10 +113,10 @@ valueExpression -> # A number or a function of a number number -> - %number {% d => new ConstantNode({value: d[0].value, type 'number'}) %} + %number {% d => new ConstantNode({value: d[0].value, type: 'number'}) %} name -> - %name {% d => new SymbolNode(d[0].value) %} + %name {% d => new SymbolNode({name: d[0].value}) %} string -> %string {% d => new ConstantNode({value: d[0].value, type: 'string'}) %} diff --git a/app/imports/parser/parseTree.js b/app/imports/parser/parseTree.js deleted file mode 100644 index 4002f7cc..00000000 --- a/app/imports/parser/parseTree.js +++ /dev/null @@ -1,96 +0,0 @@ -// All the classes that make up a parse tree - -class ParseNode { - // Compiling a node must return a ConstantNode - compile(){ - throw new Meteor.Error('Compile not implemented on ' + this); - } - compileToSingleValue(){ - return this.compile(); - } -} - -function sum(total, num) { - return total + num; -} - -class ConstantNode extends ParseNode { - constructor({value, type, errors}){ - // string, number, boolean, numberArray, uncompiledNode - this.type = type; - this.value = value; - if (errors) this.errors = errors; - } - compile(){ - return this; - } - compileToSingleValue(){ - if (this.type !== 'numberArray') return this; - return this.value.reduce(sum, 0); - } -} - -class SymbolNode extends ParseNode { - constructor({name}){ - this.name = name; - } - compile(scope){ - let value = scope[this.name]; - let type = typeof value; - if (type === 'string' || type === 'number' || type === 'boolean'){ - return new ConstantNode({value, type}); - } else if (type === 'undefined'){ - return new ConstantNode({ - value: this.name, - type: 'uncompiledNode', - errors: [`${this.name} could not be resolved`] - }); - } else { - throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); - } - } -} - -class ifNode extends ParseNode { - constructor({condition, consequent, alternative}){ - this.condition = condition; - this.consequent = consequent; - this.alternative = alternative; - } - compile(){ - let condition = this.condition.compile(); - let consequent = this.consequent.compile(); - let alternative = this.alternative.compile(); - if ( - condition.type === 'uncompiledNode' || - consequent.type === 'uncompiledNode' || - alternative.type === 'uncompiledNode' - ){ - // Handle uncompiled child nodes - return new ConstantNode({ - value: `if (${condition.value}) ${consequent.value} else ${alternative.value}`, - type: 'uncompiledNode', - errors: [ - ...condition.errors, - ...consequent.errors, - ...alternative.errors, - ], - }); - } else { - if (condition.value){ - return consequent; - } else { - return alternative; - } - } - } -} - -class OperatorNode extends ParseNode { - constructor({left, right, operator, fn}) { - this.left = left; - this.right = right; - this.fn = fn; - this.operator = operator; - } -} diff --git a/app/imports/parser/parseTree/CallNode.js b/app/imports/parser/parseTree/CallNode.js new file mode 100644 index 00000000..8ad1dfb5 --- /dev/null +++ b/app/imports/parser/parseTree/CallNode.js @@ -0,0 +1 @@ +//TODO diff --git a/app/imports/parser/parseTree/ConstantNode.js b/app/imports/parser/parseTree/ConstantNode.js new file mode 100644 index 00000000..caa910f0 --- /dev/null +++ b/app/imports/parser/parseTree/ConstantNode.js @@ -0,0 +1,21 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class ConstantNode extends ParseNode { + constructor({value, type, errors}){ + super(); + // string, number, boolean, numberArray, uncompiledNode + this.type = type; + this.value = value; + if (errors) this.errors = errors; + } + compile(){ + return this; + } + reduce(){ + if (this.type === 'numberArray'){ + return this.value.reduce((total, num) => total + num, 0); + } else { + return this; + } + } +} diff --git a/app/imports/parser/parseTree/IfNode.js b/app/imports/parser/parseTree/IfNode.js new file mode 100644 index 00000000..29bca520 --- /dev/null +++ b/app/imports/parser/parseTree/IfNode.js @@ -0,0 +1,40 @@ +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(); + this.condition = condition; + this.consequent = consequent; + this.alternative = alternative; + } + 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: `if (${condition.value}) ${consequent.value} else ${alternative.value}`, + type: 'uncompiledNode', + errors: [ + ...condition.errors, + ...consequent.errors, + ...alternative.errors, + ], + }); + } else { + // So long as the condition reolves, return the correct alternative, + // even if it's unresolved + if (condition.value){ + return consequent; + } else { + return alternative; + } + } + } +} diff --git a/app/imports/parser/parseTree/OperatorNode.js b/app/imports/parser/parseTree/OperatorNode.js new file mode 100644 index 00000000..cd4d9146 --- /dev/null +++ b/app/imports/parser/parseTree/OperatorNode.js @@ -0,0 +1,11 @@ +import ParseNode from '/imports/parser/parseTree/ParseNode.js'; + +export default class OperatorNode extends ParseNode { + constructor({left, right, operator, fn}) { + super(); + this.left = left; + this.right = right; + this.fn = fn; + this.operator = operator; + } +} diff --git a/app/imports/parser/parseTree/ParseNode.js b/app/imports/parser/parseTree/ParseNode.js new file mode 100644 index 00000000..0e213587 --- /dev/null +++ b/app/imports/parser/parseTree/ParseNode.js @@ -0,0 +1,14 @@ +export default class ParseNode { + // Compiling a node must return a ConstantNode + compile(){ + throw new Meteor.Error('Compile not implemented on ' + this); + } + // Compile, but turn rolls into arrays + roll(){ + return this.compile(); + } + // Compile, turn rolls into arrays, and reduce those arrays into single values + reduce(){ + return this.compileAndRoll() + } +} diff --git a/app/imports/parser/parseTree/RollNode.js b/app/imports/parser/parseTree/RollNode.js new file mode 100644 index 00000000..e69de29b diff --git a/app/imports/parser/parseTree/SymbolNode.js b/app/imports/parser/parseTree/SymbolNode.js new file mode 100644 index 00000000..52ce13fa --- /dev/null +++ b/app/imports/parser/parseTree/SymbolNode.js @@ -0,0 +1,24 @@ +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(); + this.name = name; + } + compile(scope){ + let value = scope && scope[this.name]; + let type = typeof value; + if (type === 'string' || type === 'number' || type === 'boolean'){ + return new ConstantNode({value, type}); + } else if (type === 'undefined'){ + return new ConstantNode({ + value: this.name, + type: 'uncompiledNode', + errors: [`${this.name} could not be resolved`] + }); + } else { + throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); + } + } +} diff --git a/app/package-lock.json b/app/package-lock.json index 367ddeae..32e4e06d 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -104,7 +104,7 @@ }, "block-stream": { "version": "0.0.9", - "resolved": false, + "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", "integrity": "sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo=", "requires": { "inherits": "~2.0.0" @@ -513,7 +513,7 @@ }, "inherits": { "version": "2.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" }, "ini": { @@ -1456,37 +1456,6 @@ "requires": { "inherits": "~2.0.1", "readable-stream": "^2.0.2" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "stream-http": { @@ -1499,37 +1468,6 @@ "readable-stream": "^2.3.3", "to-arraybuffer": "^1.0.0", "xtend": "^4.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", - "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } - } - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - } - } } }, "string_decoder": { @@ -1928,7 +1866,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": false, + "resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=" } }