Continued work on parser, now calling functions and rolling correctly

This commit is contained in:
Stefan Zermatten
2020-09-10 00:14:24 +02:00
parent 81645df2a6
commit ede4e1367d
16 changed files with 243 additions and 88 deletions

View File

@@ -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", "_", (lexer.has("exponentOperator") ? {type: "exponentOperator"} : exponentOperator), "_", "exponentExpression"], "postprocess": d => operator(d, 'exponent')},
{"name": "exponentExpression", "symbols": ["callExpression"], "postprocess": id}, {"name": "exponentExpression", "symbols": ["callExpression"], "postprocess": id},
{"name": "callExpression", "symbols": ["name", "_", "arguments"], "postprocess": {"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": "callExpression", "symbols": ["indexExpression"], "postprocess": id},
{"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]}, {"name": "arguments$ebnf$1$subexpression$1", "symbols": ["expression"], "postprocess": d => d[0]},

View File

@@ -103,7 +103,7 @@ exponentExpression ->
callExpression -> callExpression ->
name _ arguments {% 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 %} | indexExpression {% id %}

View File

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

View File

@@ -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;
}

View File

@@ -1,12 +1,12 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js'; import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ConstantNode extends ParseNode { export default class ConstantNode extends ParseNode {
constructor({value, type, errors}){ constructor({value, type, rollTable}){
super(); super(...arguments);
// string, number, boolean, numberArray, uncompiledNode // string, number, boolean, numberArray, uncompiledNode
this.type = type; this.type = type;
this.value = value; this.value = value;
if (errors) this.errors = errors; if (rollTable) this.rollTable = rollTable;
} }
compile(){ compile(){
return this; return this;
@@ -18,6 +18,6 @@ export default class ConstantNode extends ParseNode {
return this.type === 'number'; return this.type === 'number';
} }
get isInteger(){ get isInteger(){
return this.isNumberNode && Number.isInteger(this.value); return this.type === 'number' && Number.isInteger(this.value);
} }
} }

View File

@@ -2,7 +2,7 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ErrorNode extends ParseNode { export default class ErrorNode extends ParseNode {
constructor({node, error}) { constructor({node, error}) {
super(); super(...arguments);
this.node = node; this.node = node;
this.error = error; this.error = error;
} }

View File

@@ -1,9 +1,8 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js'; import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class IfNode extends ParseNode { export default class IfNode extends ParseNode {
constructor({condition, consequent, alternative}){ constructor({condition, consequent, alternative}){
super(); super(...arguments);
this.condition = condition; this.condition = condition;
this.consequent = consequent; this.consequent = consequent;
this.alternative = alternative; this.alternative = alternative;
@@ -12,33 +11,26 @@ export default class IfNode extends ParseNode {
let {condition, consequent, alternative} = this; let {condition, consequent, alternative} = this;
return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}` return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}`
} }
compile(){ compile(scope){
let condition = this.condition.compile(); return this.resolve('compile', scope);
let consequent = this.consequent.compile(); }
let alternative = this.alternative.compile(); roll(scope){
if ( return this.resolve('roll', scope);
condition.type !== 'string' && }
condition.type !== 'number' && reduce(scope){
condition.type !== 'boolean' return this.resolve('reduce', scope);
){ }
// Handle unresolved condition resolve(fn, scope){
return new ConstantNode({ let condition = this.condition[fn](scope);
value: `${condition.value}) ${consequent.value} else ${alternative.value}`, let consequent = this.consequent[fn](scope);
type: 'uncompiledNode', let alternative = this.alternative[fn](scope);
errors: [ this.resolve(condition, consequent, alternative);
...condition.errors, if (condition.value){
...consequent.errors, consequent.inheritDetails([condition, this]);
...alternative.errors, return consequent;
],
});
} else { } else {
// So long as the condition reolves, return the correct alternative, alternative.inheritDetails([condition, this]);
// even if it's unresolved return alternative;
if (condition.value){
return consequent;
} else {
return alternative;
}
} }
} }
} }

View File

@@ -2,23 +2,35 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class IndexNode extends ParseNode { export default class IndexNode extends ParseNode {
constructor({array, index}) { constructor({array, index}) {
super(); super(...arguments);
this.array = array; this.array = array;
this.index = index; this.index = index;
} }
compile(){ resolve(fn, scope){
let index = this.index.compile(); let index = this.index[fn](scope);
if (index.constructor.name === 'ConstantNode' && index.type === 'number'){ if (index.isInteger){
let selection = this.array.values[index.value]; let selection = this.array.values[index.value - 1];
if (selection){ if (selection){
return selection.compile(); let result = selection[fn](scope);
result.inheritDetails([index, this]);
return result;
} }
} }
return new IndexNode({ return new IndexNode({
array: this.array.compile(), array: this.array[fn](scope),
index: this.index.compile(), 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(){ toString(){
return `${this.array.toString()}[${this.index.toString()}]`; return `${this.array.toString()}[${this.index.toString()}]`;
} }

View File

@@ -3,22 +3,32 @@ import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class OperatorNode extends ParseNode { export default class OperatorNode extends ParseNode {
constructor({left, right, operator, fn}) { constructor({left, right, operator, fn}) {
super(); super(...arguments);
this.left = left; this.left = left;
this.right = right; this.right = right;
this.fn = fn; this.fn = fn;
this.operator = operator; this.operator = operator;
} }
compile(){ compile(scope){
let leftNode = this.left.compile(); return this.resolve('compile', scope);
let rightNode = this.right.compile(); }
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; let left, right;
if (leftNode.type !== 'number' || rightNode.type !== 'number'){ if (leftNode.type !== 'number' || rightNode.type !== 'number'){
return new OperatorNode({ return new OperatorNode({
left: leftNode, left: leftNode,
right: rightNode, right: rightNode,
operator: this.operator, operator: this.operator,
fn: this.fn fn: this.fn,
previousNodes: [this],
}); });
} else { } else {
left = leftNode.value; left = leftNode.value;
@@ -44,7 +54,11 @@ export default class OperatorNode extends ParseNode {
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}); return new ConstantNode({
value: result,
type: typeof result,
previousNodes: [this, leftNode, rightNode],
});
} }
toString(){ toString(){
let {left, right, operator} = this; let {left, right, operator} = this;

View File

@@ -2,15 +2,24 @@ import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ParenthesisNode extends ParseNode { export default class ParenthesisNode extends ParseNode {
constructor({content}) { constructor({content}) {
super(); super(...arguments);
this.content = content; this.content = content;
} }
compile(){ compile(scope){
let content = this.content.compile(); 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'){ if (content.constructor.name === 'ConstantNode'){
return content; return content;
} else { } else {
return new ParenthesisNode({content}); return new ParenthesisNode({content, previousNodes: [this]});
} }
} }
toString(){ toString(){

View File

@@ -1,4 +1,17 @@
export default class ParseNode { 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(){ compile(){
// Returns a ParseNode, a ConstantNode if possible // Returns a ParseNode, a ConstantNode if possible
throw new Meteor.Error('Compile not implemented on ' + this.constructor.name); 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); throw new Meteor.Error('toString not implemented on ' + this.constructor.name);
} }
// Compile, but turn rolls into arrays // Compile, but turn rolls into arrays
roll(){ roll(scope){
return this.compile(); return this.compile(scope);
} }
// Compile, turn rolls into arrays, and reduce those arrays into single values // Compile, turn rolls into arrays, and reduce those arrays into single values
reduce(){ reduce(scope){
return this.roll(); return this.roll(scope);
} }
} }

View File

@@ -1,8 +1,9 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js'; import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class RollArrayNode extends ParseNode { export default class RollArrayNode extends ParseNode {
constructor({values}) { constructor({values}) {
super(); super(...arguments);
this.values = values; this.values = values;
} }
compile(){ compile(){
@@ -12,6 +13,11 @@ export default class RollArrayNode extends ParseNode {
return `[${this.values.join(', ')}]`; return `[${this.values.join(', ')}]`;
} }
reduce(){ reduce(){
//TODO sum and return values let total = this.values.reduce((a, b) => a + b);
return new ConstantNode({
value: total,
type: 'number',
previousNodes: [this],
});
} }
} }

View File

@@ -1,17 +1,17 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js'; 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'; import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
export default class RollNode extends ParseNode { export default class RollNode extends ParseNode {
constructor({left, right}) { constructor({left, right}) {
super(); super(...arguments);
this.left = left; this.left = left;
this.right = right; this.right = right;
} }
compile(){ compile(scope){
let left = this.left.compile(); let left = this.left.compile(scope);
let right = this.right.compile(); let right = this.right.compile(scope);
return new RollNode({left, right}); return new RollNode({left, right, previousNodes: [this]});
} }
toString(){ toString(){
if ( if (
@@ -22,19 +22,21 @@ export default class RollNode extends ParseNode {
return `${this.left.toString()}d${this.right.toString()}`; return `${this.left.toString()}d${this.right.toString()}`;
} }
} }
roll(){ roll(scope){
let left = this.left.reduce(); let left = this.left.reduce(scope);
let right = this.right.reduce(); let right = this.right.reduce(scope);
if (!left.isInteger){ if (!left.isInteger){
return new ErrorNode({ return new ErrorNode({
node: this, 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){ if (!right.isInteger){
return new ErrorNode({ return new ErrorNode({
node: this, node: this,
error: 'Dice size is not an integer' error: 'Dice size is not an integer',
previousNodes: [this, left, right],
}); });
} }
let number = left.value; let number = left.value;
@@ -44,14 +46,18 @@ export default class RollNode extends ParseNode {
}); });
let diceSize = right.value; let diceSize = right.value;
let randomSrc = DDP.randomStream('diceRoller'); let randomSrc = DDP.randomStream('diceRoller');
let rolls = []; let values = [];
for (let i = 0; i < number; i++){ for (let i = 0; i < number; i++){
let roll = ~~(randomSrc.fraction() * diceSize) + 1 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(){ reduce(scope){
this.roll().reduce(); return this.roll(scope).reduce(scope);
} }
} }

View File

@@ -1,9 +1,10 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js'; import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js'; import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
export default class SymbolNode extends ParseNode { export default class SymbolNode extends ParseNode {
constructor({name}){ constructor({name}){
super(); super(...arguments);
this.name = name; this.name = name;
} }
toString(){ toString(){
@@ -13,12 +14,11 @@ export default class SymbolNode extends ParseNode {
let value = scope && scope[this.name]; let value = scope && scope[this.name];
let type = typeof value; let type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean'){ if (type === 'string' || type === 'number' || type === 'boolean'){
return new ConstantNode({value, type}); return new ConstantNode({value, type, previousNodes: [this]});
} else if (type === 'undefined'){ } else if (type === 'undefined'){
return new ConstantNode({ return new ErrorNode({
value: this.name, node: this,
type: 'uncompiledNode', error: `${this.name} could not be resolved`,
errors: [`${this.name} could not be resolved`]
}); });
} else { } else {
throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`); throw new Meteor.Error(`Unexpected case: ${this.name} resolved to ${value}`);

View File

@@ -18,6 +18,21 @@
readonly readonly
label="compiled" label="compiled"
/> />
<v-text-field
v-model="rolled"
readonly
label="rolled"
/>
<v-text-field
v-model="reduced"
readonly
label="reduced"
/>
<v-textarea
v-model="reducedJson"
readonly
label="reduced"
/>
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
@@ -32,6 +47,9 @@ export default {
output: null, output: null,
compiled: null, compiled: null,
string: null, string: null,
rolled: null,
reduced: null,
reducedJson: null,
}}, }},
watch: { watch: {
input(val){ input(val){
@@ -41,7 +59,11 @@ export default {
this.output = JSON.stringify(output, null, 2); this.output = JSON.stringify(output, null, 2);
if (!output) return; if (!output) return;
this.string = output; 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)
} }
}, },
} }

2
app/package-lock.json generated
View File

@@ -2805,7 +2805,7 @@
}, },
"signal-exit": { "signal-exit": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", "resolved": false,
"integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0="
}, },
"simpl-schema": { "simpl-schema": {