Lots of work on the parser including testing interface
This commit is contained in:
15
app/imports/parser/parseTree/ArrayNode.js
Normal file
15
app/imports/parser/parseTree/ArrayNode.js
Normal 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(', ')}]`;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
15
app/imports/parser/parseTree/ErrorNode.js
Normal file
15
app/imports/parser/parseTree/ErrorNode.js
Normal 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 '###';
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
25
app/imports/parser/parseTree/IndexNode.js
Normal file
25
app/imports/parser/parseTree/IndexNode.js
Normal 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()}]`;
|
||||
}
|
||||
}
|
||||
@@ -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()}`;
|
||||
}
|
||||
}
|
||||
|
||||
19
app/imports/parser/parseTree/ParenthesisNode.js
Normal file
19
app/imports/parser/parseTree/ParenthesisNode.js
Normal 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()})`;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
17
app/imports/parser/parseTree/RollArrayNode.js
Normal file
17
app/imports/parser/parseTree/RollArrayNode.js
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user