Lots of work on the parser including testing interface

This commit is contained in:
Stefan Zermatten
2020-09-09 17:09:50 +02:00
parent 445171ce80
commit 81645df2a6
15 changed files with 341 additions and 39 deletions

View File

@@ -2,11 +2,16 @@
// http://github.com/Hardmath123/nearley
function id(x) { return x[0]; }
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 IfNode from '/imports/parser/parseTree/IfNode.js';
import IndexNode from '/imports/parser/parseTree/IndexNode.js';
import OperatorNode from '/imports/parser/parseTree/OperatorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.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 moo from 'moo';
const lexer = moo.compile({
@@ -25,7 +30,8 @@ function id(x) { return x[0]; }
match: /\s+/,
lineBreaks: true,
},
separators: [',', '.'],
separator: [',', ';'],
period: ['.'],
multiplicativeOperator: ['*', '/'],
exponentOperator: ['^'],
additiveOperator: ['+', '-'],
@@ -35,7 +41,7 @@ function id(x) { return x[0]; }
stringDelimiters: ['\"', '\''],
equalityOperator: ['=', '==', '===', '!=', '!=='],
relationalOperator: ['>', '<', '>=', '<='],
brackets: ['(', ')', '{', '}'],
brackets: ['(', ')', '{', '}', '[', ']'],
});
function nuller() { return null; }
@@ -49,11 +55,11 @@ function id(x) { return x[0]; }
}
let Lexer = lexer;
let ParserRules = [
{"name": "ifStatement", "symbols": [{"literal":"if"}, "_", {"literal":"("}, "_", "expression", "_", {"literal":")"}, "_", "ifStatement", "_", {"literal":"else"}, "_", "ifStatement"], "postprocess":
{"name": "expression", "symbols": ["ifStatement"], "postprocess": d => d[0]},
{"name": "ifStatement", "symbols": ["_", "expression", "_", {"literal":"?"}, "_", "expression", "_", {"literal":":"}, "_", "expression"], "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": "ifStatement", "symbols": ["equalityExpression"], "postprocess": id},
{"name": "equalityExpression", "symbols": ["equalityExpression", "_", (lexer.has("equalityOperator") ? {type: "equalityOperator"} : equalityOperator), "_", "relationalExpression"], "postprocess": d => operator(d, 'equality')},
{"name": "equalityExpression", "symbols": ["relationalExpression"], "postprocess": id},
{"name": "relationalExpression", "symbols": ["relationalExpression", "_", (lexer.has("relationalOperator") ? {type: "relationalOperator"} : relationalOperator), "_", "orExpression"], "postprocess": d => operator(d, 'relation')},
@@ -66,33 +72,47 @@ let ParserRules = [
{"name": "additiveExpression", "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", "_", {"literal":"d"}, "_", "exponentExpression"], "postprocess": d => operator(d, 'roll')},
{"name": "rollExpression", "symbols": ["exponentExpression"], "postprocess": id},
{"name": "rollExpression", "symbols": ["rollExpression", "_", {"literal":"d"}, "_", "exponentExpression"], "postprocess": d => new RollNode({left: d[0], right: d[4]})},
{"name": "rollExpression", "symbols": ["singleRollExpression"], "postprocess": id},
{"name": "singleRollExpression", "symbols": [{"literal":"d"}, "_", "exponentExpression"], "postprocess": d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), 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": ["callExpression"], "postprocess": id},
{"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess":
d => new CallNode ({type: "call", fn: d[0], arguments: d[2]})
},
{"name": "callExpression", "symbols": ["parenthesizedExpression"], "postprocess": id},
{"name": "callExpression", "symbols": ["indexExpression"], "postprocess": id},
{"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},
{"name": "arguments$ebnf$1", "symbols": ["arguments$ebnf$1$subexpression$1"], "postprocess": id},
{"name": "arguments$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "arguments$ebnf$2", "symbols": []},
{"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", {"literal":","}, "_", "expression"], "postprocess": d => d[3]},
{"name": "arguments$ebnf$2$subexpression$1", "symbols": ["_", (lexer.has("separator") ? {type: "separator"} : separator), "_", "expression"], "postprocess": d => d[3]},
{"name": "arguments$ebnf$2", "symbols": ["arguments$ebnf$2", "arguments$ebnf$2$subexpression$1"], "postprocess": function arrpush(d) {return d[0].concat([d[1]]);}},
{"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"], "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},
{"name": "arrayExpression$ebnf$1", "symbols": [], "postprocess": function(d) {return null;}},
{"name": "arrayExpression$ebnf$2", "symbols": []},
{"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[3]]})
},
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => d[2]},
{"name": "arrayExpression", "symbols": ["parenthesizedExpression"], "postprocess": id},
{"name": "parenthesizedExpression", "symbols": [{"literal":"("}, "_", "expression", "_", {"literal":")"}], "postprocess": d => new ParenthesisNode({content: d[2]})},
{"name": "parenthesizedExpression", "symbols": ["valueExpression"], "postprocess": id},
{"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({value: d[0].value, type: 'number'})},
{"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}
];
let ParserStart = "ifStatement";
let ParserStart = "expression";
export default { Lexer, ParserRules, ParserStart };

View File

@@ -1,10 +1,15 @@
@preprocessor esmodule
@{%
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 IfNode from '/imports/parser/parseTree/IfNode.js';
import IndexNode from '/imports/parser/parseTree/IndexNode.js';
import OperatorNode from '/imports/parser/parseTree/OperatorNode.js';
import SymbolNode from '/imports/parser/parseTree/SymbolNode.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 moo from 'moo';
const lexer = moo.compile({
@@ -23,7 +28,8 @@
match: /\s+/,
lineBreaks: true,
},
separators: [',', '.'],
separator: [',', ';'],
period: ['.'],
multiplicativeOperator: ['*', '/'],
exponentOperator: ['^'],
additiveOperator: ['+', '-'],
@@ -33,7 +39,7 @@
stringDelimiters: ['\"', '\''],
equalityOperator: ['=', '==', '===', '!=', '!=='],
relationalOperator: ['>', '<', '>=', '<='],
brackets: ['(', ')', '{', '}'],
brackets: ['(', ')', '{', '}', '[', ']'],
});
function nuller() { return null; }
@@ -50,14 +56,14 @@
# Use the Moo lexer
@lexer lexer
expression ->
ifStatement {% d => d[0] %}
ifStatement ->
"if" _ "(" _ expression _ ")" _ ifStatement _ "else" _ ifStatement {%
_ expression _ "?" _ expression _ ":" _ expression {%
d => new IfNode({condition: d[4], consequent: d[8], alternative: d[12]})
%}
| expression {% id %}
expression ->
equalityExpression {% d => d[0] %}
| equalityExpression {% id %}
equalityExpression ->
equalityExpression _ %equalityOperator _ relationalExpression {% d => operator(d, 'equality') %}
@@ -84,7 +90,11 @@ multiplicativeExpression ->
| rollExpression {% id %}
rollExpression ->
rollExpression _ "d" _ exponentExpression {% d => operator(d, 'roll') %}
rollExpression _ "d" _ exponentExpression {% d => new RollNode({left: d[0], right: d[4]}) %}
| singleRollExpression {% id %}
singleRollExpression ->
"d" _ exponentExpression {% d => new RollNode({left: new ConstantNode({value: 1, type: 'number'}), right: d[2]}) %}
| exponentExpression {% id %}
exponentExpression ->
@@ -95,15 +105,25 @@ callExpression ->
name _ arguments {%
d => new CallNode ({type: "call", fn: d[0], arguments: d[2]})
%}
| parenthesizedExpression {% id %}
| indexExpression {% id %}
arguments ->
"(" _ (expression {% d => d[0] %}):? ( _ "," _ expression {% d => d[3] %} ):* _ ")" {%
d => [d[2], ...d[3]]
"(" _ (expression {% d => d[0] %}):? ( _ %separator _ expression {% d => d[3] %} ):* _ ")" {%
d => [d[2], ...d[3]]
%}
indexExpression ->
arrayExpression "[" _ expression _ "]" {% d => new IndexNode ({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[3]]})
%}
| parenthesizedExpression {% id %}
parenthesizedExpression ->
"(" _ expression _ ")" {% d => d[2] %}
"(" _ expression _ ")" {% d => new ParenthesisNode({content: d[2]}) %}
| valueExpression {% id %}
valueExpression ->
@@ -113,7 +133,7 @@ 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({name: d[0].value}) %}

View File

@@ -0,0 +1,15 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ArrayNode extends ParseNode {
constructor({values}) {
super();
this.values = values;
}
compile(){
let values = this.values.map(node => node.compile());
return new ArrayNode({values});
}
toString(){
return `[${this.values.map(node => node.toString()).join(', ')}]`;
}
}

View File

@@ -11,11 +11,13 @@ export default class ConstantNode extends ParseNode {
compile(){
return this;
}
reduce(){
if (this.type === 'numberArray'){
return this.value.reduce((total, num) => total + num, 0);
} else {
return this;
}
toString(){
return `${this.value}`;
}
get isNumber(){
return this.type === 'number';
}
get isInteger(){
return this.isNumberNode && Number.isInteger(this.value);
}
}

View File

@@ -0,0 +1,15 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ErrorNode extends ParseNode {
constructor({node, error}) {
super();
this.node = node;
this.error = error;
}
compile(){
return this;
}
toString(){
return '###';
}
}

View File

@@ -8,6 +8,10 @@ export default class IfNode extends ParseNode {
this.consequent = consequent;
this.alternative = alternative;
}
toString(){
let {condition, consequent, alternative} = this;
return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}`
}
compile(){
let condition = this.condition.compile();
let consequent = this.consequent.compile();
@@ -19,7 +23,7 @@ export default class IfNode extends ParseNode {
){
// Handle unresolved condition
return new ConstantNode({
value: `if (${condition.value}) ${consequent.value} else ${alternative.value}`,
value: `${condition.value}) ${consequent.value} else ${alternative.value}`,
type: 'uncompiledNode',
errors: [
...condition.errors,

View File

@@ -0,0 +1,25 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class IndexNode extends ParseNode {
constructor({array, index}) {
super();
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];
if (selection){
return selection.compile();
}
}
return new IndexNode({
array: this.array.compile(),
index: this.index.compile(),
});
}
toString(){
return `${this.array.toString()}[${this.index.toString()}]`;
}
}

View File

@@ -1,4 +1,5 @@
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}) {
@@ -8,4 +9,45 @@ export default class OperatorNode extends ParseNode {
this.fn = fn;
this.operator = operator;
}
compile(){
let leftNode = this.left.compile();
let rightNode = this.right.compile();
let left, right;
if (leftNode.type !== 'number' || rightNode.type !== 'number'){
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 '&':
case '&&': result = left && right; break;
case '|':
case '||': result = left || right; break;
case '=':
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;
}
return new ConstantNode({value: result, type: typeof result});
}
toString(){
let {left, right, operator} = this;
return `${left.toString()} ${operator} ${right.toString()}`;
}
}

View File

@@ -0,0 +1,19 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ParenthesisNode extends ParseNode {
constructor({content}) {
super();
this.content = content;
}
compile(){
let content = this.content.compile();
if (content.constructor.name === 'ConstantNode'){
return content;
} else {
return new ParenthesisNode({content});
}
}
toString(){
return `(${this.content.toString()})`;
}
}

View File

@@ -1,7 +1,10 @@
export default class ParseNode {
// Compiling a node must return a ConstantNode
compile(){
throw new Meteor.Error('Compile not implemented on ' + this);
// Returns a ParseNode, a ConstantNode if possible
throw new Meteor.Error('Compile not implemented on ' + this.constructor.name);
}
toString(){
throw new Meteor.Error('toString not implemented on ' + this.constructor.name);
}
// Compile, but turn rolls into arrays
roll(){
@@ -9,6 +12,6 @@ export default class ParseNode {
}
// Compile, turn rolls into arrays, and reduce those arrays into single values
reduce(){
return this.compileAndRoll()
return this.roll();
}
}

View File

@@ -0,0 +1,17 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class RollArrayNode extends ParseNode {
constructor({values}) {
super();
this.values = values;
}
compile(){
return this;
}
toString(){
return `[${this.values.join(', ')}]`;
}
reduce(){
//TODO sum and return values
}
}

View File

@@ -0,0 +1,57 @@
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 RollNode extends ParseNode {
constructor({left, right}) {
super();
this.left = left;
this.right = right;
}
compile(){
let left = this.left.compile();
let right = this.right.compile();
return new RollNode({left, right});
}
toString(){
if (
this.left.isNumberNode && this.left.value === 1
){
return `d${this.right.toString()}`;
} else {
return `${this.left.toString()}d${this.right.toString()}`;
}
}
roll(){
let left = this.left.reduce();
let right = this.right.reduce();
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 (number > 100) return new ErrorNode({
node: this,
error: 'Can\'t roll more than 100 dice at once'
});
let diceSize = right.value;
let randomSrc = DDP.randomStream('diceRoller');
let rolls = [];
for (let i = 0; i < number; i++){
let roll = ~~(randomSrc.fraction() * diceSize) + 1
rolls.push(roll);
}
return new ArrayNode({values: rolls});
}
reduce(){
this.roll().reduce();
}
}

View File

@@ -6,6 +6,9 @@ export default class SymbolNode extends ParseNode {
super();
this.name = name;
}
toString(){
return `${this.name}`
}
compile(scope){
let value = scope && scope[this.name];
let type = typeof value;

View File

@@ -0,0 +1,51 @@
<template lang="html">
<div class="layout column align-center justify-center pa-4">
<v-card style="width: 100%; max-width: 400px;">
<v-card-text>
<v-text-field v-model="input" />
<v-textarea
v-model="output"
readonly
label="output"
/>
<v-text-field
v-model="string"
readonly
label="string"
/>
<v-text-field
v-model="compiled"
readonly
label="compiled"
/>
</v-card-text>
</v-card>
</div>
</template>
<script>
import Parser from '/imports/parser/parser.js';
console.log(Parser);
export default {
data(){return {
input: null,
output: null,
compiled: null,
string: null,
}},
watch: {
input(val){
this.output = this.compiled = this.string = '';
let output = new Parser().feed(val).finish()[0];
console.log(output);
this.output = JSON.stringify(output, null, 2);
if (!output) return;
this.string = output;
this.compiled = output.compile();
}
},
}
</script>
<style lang="css" scoped>
</style>

View File

@@ -23,6 +23,7 @@ import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
import Tabletops from '/imports/ui/pages/Tabletops.vue';
import Tabletop from '/imports/ui/pages/Tabletop.vue';
import TabletopToolbar from '/imports/ui/tabletop/TabletopToolbar.vue';
import Parser from '/imports/ui/pages/Parser.vue';
let userSubscription = Meteor.subscribe('user');
@@ -109,6 +110,14 @@ RouterFactory.configure(factory => {
title: 'Character List',
},
beforeEnter: ensureLoggedIn,
},{
path: '/parser',
components: {
default: Parser,
},
meta: {
title: 'Parser',
},
},{
path: '/library',
components: {