Began work on rewriting parser without object orientation

Parsing is expensive, if the parse tree can be stored on the DB it can 
save a lot of compute time, but mongo can't store Classes, so we 
re-write without classes
This commit is contained in:
Stefan Zermatten
2021-10-01 13:41:22 +02:00
parent cb1fd38df3
commit feffa45cf7
19 changed files with 521 additions and 305 deletions

View File

@@ -0,0 +1,12 @@
// Collate the array with the given value or array of values, creating the
// array if it doesn't exist
export default function collate(array, toAdd){
if (Array.isArray(toAdd) && toAdd.length){
if (!array) array = [];
array.push(...toAdd);
} else if (toAdd) {
if (!array) array = [];
array.push(toAdd);
}
return array;
}

View File

@@ -0,0 +1,20 @@
export default class Context {
constructor({errors = [], rolls = []}){
this.errors = errors;
this.rolls = rolls;
}
error(e){
if (!e) return;
if (typeof e === 'string'){
this.errors.push({
type: 'error',
message: e,
});
} else {
this.errors.push(e);
}
}
roll(r){
this.rolls.push(r);
}
}

View File

@@ -1 +0,0 @@
// Takes a parse tree and computes it down as far as possible into a real number

View File

@@ -1,54 +0,0 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class AccessorNode extends ParseNode {
constructor({name, path}) {
super(...arguments);
this.name = name;
this.path = path;
}
compile(scope, context){
let value = scope && scope[this.name];
// For objects, get their value
this.path.forEach(name => {
if (value === undefined) return;
value = value[name];
});
let type = typeof value;
if (type === 'string' || type === 'number' || type === 'boolean'){
return new ConstantNode({value, type});
} else if (type === 'undefined'){
return new AccessorNode({
name: this.name,
path: this.path,
});
} else {
if (context) context.storeError({
type: 'error',
message: `${this.name} returned an unexpected type`
});
return new AccessorNode({
name: this.name,
path: this.path,
});
}
}
reduce(scope, context){
let result = this.compile(scope, context);
if (result instanceof AccessorNode){
if (context) context.storeError({
type: 'info',
message: `${result.toString()} not found, set to 0`
});
return new ConstantNode({
type: 'number',
value: 0,
});
} else {
return result;
}
}
toString(){
return `${this.name}.${this.path.join('.')}`;
}
}

View File

@@ -1,39 +0,0 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
export default class ArrayNode extends ParseNode {
constructor({values}) {
super(...arguments);
this.values = values;
}
static fromConstantArray(array){
let values = array.map( value => {
let type = typeof value;
if (
type === 'string' ||
type === 'number' ||
type === 'boolean' ||
type === 'undefined'
){
return new ConstantNode({value, type});
} else {
throw `Unexpected type in constant array: ${type}`
}
});
return new ArrayNode({values});
}
resolve(fn, scope, context){
let values = this.values.map(node => node[fn](scope, context));
return new ArrayNode({values});
}
toString(){
return `[${this.values.map(node => node.toString()).join(', ')}]`;
}
traverse(fn){
fn(this);
this.values.forEach(value => value.traverse(fn));
}
replaceChildren(fn){
this.values = this.values.map(node => node.replaceNodes(fn));
}
}

View File

@@ -1,126 +0,0 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
import functions from '/imports/parser/functions.js';
export default class CallNode extends ParseNode {
constructor({functionName, args}) {
super(...arguments);
this.functionName = functionName;
this.args = args;
}
resolve(fn, scope, context){
let func = functions[this.functionName];
// Check that the function exists
if (!func) return new ErrorNode({
node: this,
error: `${this.functionName} is not a supported function`,
context,
});
// Resolve the arguments
let resolvedArgs = this.args.map(node => node[fn](scope, context));
// Check that the arguments match what is expected
let checkFailed = this.checkArugments({
fn,
context,
resolvedArgs,
argumentsExpected: func.arguments
});
if (checkFailed){
if (fn === 'reduce'){
return new ErrorNode({
node: this,
error: `Invalid arguments to ${this.functionName} function`,
});
} else {
return new CallNode({
functionName: this.functionName,
args: resolvedArgs,
});
}
}
// Map contant nodes to constants before attempting to run the function
let mappedArgs = resolvedArgs.map(node => {
if (node instanceof ConstantNode){
return node.value;
} else {
return node;
}
});
try {
// Run the function
let value = func.fn.apply(null, mappedArgs);
let type = typeof value;
if (type === 'number' || type === 'string' || type === 'boolean'){
// Convert constant results into constant nodes
return new ConstantNode({ value, type });
} else {
return value;
}
} catch (error) {
return new ErrorNode({
node: this,
error: error.message || error,
context,
});
}
}
toString(){
return `${this.functionName}(${this.args.map(node => node.toString()).join(', ')})`;
}
traverse(fn){
fn(this);
this.args.forEach(arg => arg.traverse(fn));
}
replaceChildren(fn){
this.args = this.args.map(arg => arg.replaceNodes(fn));
}
checkArugments({fn, context, argumentsExpected, resolvedArgs}){
// Check that the number of arguments matches the number expected
if (
!argumentsExpected.anyLength &&
argumentsExpected.length !== resolvedArgs.length
){
context.storeError({
type: 'error',
message: 'Incorrect number of arguments ' +
`to ${this.functionName} function, ` +
`expected ${argumentsExpected.length} got ${resolvedArgs.length}`
});
return true;
}
let failed = false;
// Check that each argument is of the correct type
resolvedArgs.forEach((node, index) => {
let type;
if (argumentsExpected.anyLength){
type = argumentsExpected[0];
} else {
type = argumentsExpected[index];
}
if (typeof type === 'string'){
// Type being a string means a constant node with matching type
if (node.type !== type) failed = true;
} else {
// Otherwise check that the node is an instance of the given type
if (!(node instanceof type)) failed = true;
}
if (failed && fn === 'reduce'){
let typeName = typeof type === 'string' ? type : type.constructor.name;
let nodeName = node.type || node.constructor.name
context.storeError({
type: 'error',
message: `Incorrect arguments to ${this.functionName} function` +
`expected ${typeName} got ${nodeName}`
});
}
});
return failed;
}
}

View File

@@ -1,22 +0,0 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ConstantNode extends ParseNode {
constructor({value, type}){
super(...arguments);
// string, number, boolean, uncompiledNode
this.type = type;
this.value = value;
}
compile(){
return this;
}
toString(){
return `${this.value}`;
}
get isNumber(){
return this.type === 'number';
}
get isInteger(){
return this.type === 'number' && Number.isInteger(this.value);
}
}

View File

@@ -1,21 +0,0 @@
import ParseNode from '/imports/parser/parseTree/ParseNode.js';
export default class ErrorNode extends ParseNode {
constructor({node, error, context}) {
super(...arguments);
this.node = node;
this.error = error;
if (context){
context.storeError({
type: 'error',
message: error,
});
}
}
compile(){
return this;
}
toString(){
return this.error.toString();
}
}

View File

@@ -1,42 +0,0 @@
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(...arguments);
this.condition = condition;
this.consequent = consequent;
this.alternative = alternative;
}
toString(){
let {condition, consequent, alternative} = this;
return `${condition.toString()} ? ${consequent.toString()} : ${alternative.toString()}`
}
resolve(fn, scope, context){
let condition = this.condition[fn](scope, context);
if (condition instanceof ConstantNode){
if (condition.value){
return this.consequent[fn](scope, context);
} else {
return this.alternative[fn](scope, context);
}
} else {
return new IfNode({
condition: condition,
consequent: this.consequent,
alternative: this.alternative,
});
}
}
traverse(fn){
fn(this);
this.condition.traverse(fn);
this.consequent.traverse(fn);
this.alternative.traverse(fn);
}
replaceChildren(fn){
this.condition = this.condition.replaceNodes(fn);
this.consequent = this.consequent.replaceNodes(fn);
this.alternative = this.alternative.replaceNodes(fn);
}
}

View File

@@ -0,0 +1,5 @@
import accessor from './accessor.js';
export default Object.freeze({
accessor,
});

View File

@@ -0,0 +1,66 @@
import constant from './constant.js';
const accessor = {
create({name, path}) {
return {
type: 'accessor',
path,
name,
};
},
compile(node, scope, context){
let value = scope && scope[node.name];
// For objects, get their value
node.path.forEach(name => {
if (value === undefined) return;
value = value[name];
});
let valueType = typeof value;
if (valueType === 'string' || valueType === 'number' || valueType === 'boolean'){
return {
result: constant.create({
value,
valueType
}),
context,
};
} else if (valueType === 'undefined'){
return {
result: accessor.create({
name: node.name,
path: node.path,
}),
context,
};
} else {
context.error(`${node.name} returned an unexpected type`);
return {
result: accessor.create({
name: node.name,
path: node.path,
}),
context,
};
}
},
reduce(node, scope, context){
let { result } = accessor.compile(node, scope, context);
if (result.type === 'accessor'){
context.error(`${accessor.toString(result)} not found, set to 0`);
return {
result: constant.create({
value: 0,
valueType: 'number',
}),
context
};
} else {
return {result, context};
}
},
toString(node){
return `${node.name}.${node.path.join('.')}`;
}
}
export default accessor;

View File

@@ -0,0 +1,46 @@
import constant from './constant.js';
import resolve, { toString, traverse } from '../resolve.js';
const array = {
create({values}) {
return {
type: 'array',
values,
};
},
fromConstantArray(array){
let values = array.map( value => {
let valueType = typeof value;
if (
valueType === 'string' ||
valueType === 'number' ||
valueType === 'boolean' ||
valueType === 'undefined'
){
return constant.create({value, valueType});
} else {
throw `Unexpected type in constant array: ${valueType}`
}
});
return array.create({values});
},
resolve(fn, node, scope){
let values = node.values.map(node => {
let { result } = resolve(fn, node, scope, context);
return result;
});
return {
result: array.create({values}),
context,
};
},
toString(node){
return `[${node.values.map(value => toString(value)).join(', ')}]`;
},
traverse(node, fn){
fn(node);
node.values.forEach(value => traverse(value, fn));
},
}
export default array;

View File

@@ -0,0 +1,146 @@
import error from './error.js';
import constant from './constant.js';
import functions from '/imports/parser/functions.js';
import resolve, { toString, traverse, mergeResolvedNodes } from '../resolve.js';
const call = {
create({functionName, args}) {
return {
type: 'call',
functionName,
args,
}
},
resolve(fn, node, scope, context){
let func = functions[node.functionName];
// Check that the function exists
if (!func) {
context.error(`${node.functionName} is not a supported function`);
return {
result: error.create({
node: node,
error: `${node.functionName} is not a supported function`,
}),
context,
};
}
// Resolve the arguments
let resolvedArgs = node.args.map(arg => {
let { result } = resolve(fn, arg, scope, context);
return result;
});
// Check that the arguments match what is expected
let checkFailed = call.checkArugments({
fn,
resolvedArgs,
argumentsExpected: func.arguments,
context,
});
if (checkFailed){
if (fn === 'reduce'){
context.error(`Invalid arguments to ${node.functionName} function`);
return {
result: error.create({
node: node,
error: `Invalid arguments to ${node.functionName} function`,
}),
context,
};
} else {
return {
result: call.create({
functionName: node.functionName,
args: resolvedArgs,
}),
context,
};
}
}
// Map contant nodes to constants before attempting to run the function
let mappedArgs = resolvedArgs.map(arg => {
if (arg.type === 'constant'){
return arg.value;
} else {
return arg;
}
});
try {
// Run the function
let value = func.fn.apply(null, mappedArgs);
let valueType = typeof value;
if (valueType === 'number' || valueType === 'string' || valueType === 'boolean'){
// Convert constant results into constant nodes
return {
result: constant.create({ value, valueType }),
context,
};
} else {
return {
result: value,
context,
};
}
} catch (error) {
context.error(error.message || error);
return {
result: error.create({
node: node,
error: error.message || error,
}),
context,
}
}
},
toString(node){
return `${node.functionName}(${node.args.map(arg => toString(arg)).join(', ')})`;
},
traverse(node, fn){
fn(node);
node.args.forEach(arg => traverse(arg, fn));
},
checkArugments({node, fn, argumentsExpected, resolvedArgs, context}){
// Check that the number of arguments matches the number expected
if (
!argumentsExpected.anyLength &&
argumentsExpected.length !== resolvedArgs.length
){
context.error('Incorrect number of arguments ' +
`to ${node.functionName} function, ` +
`expected ${argumentsExpected.length} got ${resolvedArgs.length}`);
return true;
}
let failed = false;
// Check that each argument is of the correct type
resolvedArgs.forEach((node, index) => {
let type;
if (argumentsExpected.anyLength){
type = argumentsExpected[0];
} else {
type = argumentsExpected[index];
}
if (typeof type === 'string'){
// Type being a string means a constant node with matching type
if (node.valueType !== type) failed = true;
} else {
// Otherwise check that the node is an instance of the given type
if (!(node instanceof type)) failed = true;
}
if (failed && fn === 'reduce'){
let typeName = typeof type === 'string' ? type : type.constructor.name;
let nodeName = node.type;
context.error(`Incorrect arguments to ${node.functionName} function` +
`expected ${typeName} got ${nodeName}`);
}
});
return failed;
}
}
export default call;

View File

@@ -0,0 +1,18 @@
const constant = {
create({value, valueType}){
if (!valueType) throw `Expected valueType to be set, got ${valueType}`
return {
type: 'constant',
valueType,
value,
}
},
compile(node){
return node;
},
toString(node){
return `${node.value}`;
},
}
export default constant;

View File

@@ -0,0 +1,17 @@
const error = {
create({node, error}) {
return {
type: 'error',
node,
error,
}
},
compile(node){
return node;
},
toString(node){
return node.error.toString();
},
}
export default error;

View File

@@ -0,0 +1,59 @@
import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve';
import collate from '/imports/api/engine/computation/utility/collate.js';
const ifNode = {
create({condition, consequent, alternative}){
return {
type: 'if',
condition,
consequent,
alternative,
};
},
toString(node){
let {condition, consequent, alternative} = node;
return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}`
},
resolve(fn, node, scope){
let rest, condition, consequent, alternative;
let resolved = {};
({result: condition, ...rest} = resolve(fn, node.condition, scope));
mergeResolvedNodes(resolved, rest);
if (condition.type === 'constant'){
if (condition.value){
({result: consequent, ...rest} = resolve(fn, node.consequent, scope));
mergeResolvedNodes(resolved, rest);
return {
result: consequent,
...resolved
};
} else {
({result: alternative, ...rest} = resolve(fn, node.alternative, scope));
mergeResolvedNodes(resolved, rest);
return {
result: alternative,
...resolved
};
}
} else {
return {
result: ifNode.create({
condition: condition,
consequent: node.consequent,
alternative: node.alternative,
}),
...resolved
};
}
},
traverse(node, fn){
fn(node);
traverse(node.condition, fn);
traverse(node.consequent, fn);
traverse(node.alternative, fn);
},
}
export default ifNode;

View File

@@ -0,0 +1,76 @@
import resolve, {traverse, toString, mergeResolvedNodes} from '../resolve';
const index = {
create({array, index}) {
return {
type: 'index',
array,
index,
}
},
resolve(fn, node, scope){
let index, array, rest;
let resolved = {};
({result: index, ...rest} = resolve(fn, node.index, scope));
mergeResolvedNodes(resolved, rest);
({result: array, ...rest} = resolve(fn, node.array, scope));
mergeResolvedNodes(resolved, rest);
if (
index.valueType === 'number' &&
Number.isInteger(index.value) &&
array.type === 'array'
){
if (index.value < 1 || index.value > array.values.length){
mergeResolvedNodes(resolved, {
errors: [{
type: 'warning',
message: `Index of ${index.value} is out of range for an array` +
` of length ${array.values.length}`,
}]
});
}
let selection = array.values[index.value - 1];
if (selection){
let result;
({result, ...rest} = resolve(fn, selection, scope));
mergeResolvedNodes(resolved, rest)
return result;
}
} else if (fn === 'reduce'){
if (!(array instanceof ArrayNode)){
return new ErrorNode({
node: node,
error: 'Can not get the index of a non-array node: ' +
node.array.toString() + ' = ' + array.toString(),
context,
});
} else if (!index.isInteger){
return new ErrorNode({
node: node,
error: array.toString() + ' is not an integer index of the array',
context,
});
}
}
return new IndexNode({
index,
array,
previousNodes: [node],
});
},
toString(){
return `${node.array.toString()}[${node.index.toString()}]`;
},
traverse(fn){
fn(node);
node.array.traverse(fn);
node.index.traverse(fn);
},
replaceChildren(fn){
node.array = node.array.replaceNodes(fn);
node.index = node.index.replaceNodes(fn);
}
}
export default index;

View File

@@ -0,0 +1,11 @@
import { parse } from './parser';
import { assert } from 'chai';
describe('Parser', function(){
it('parses valid text without error', function(){
assert.typeOf(parse('1'), 'object');
});
it('parses various operations', function(){
assert.typeOf(parse('1 + 2 * 3 / 4 * 1d8'), 'object');
});
});

View File

@@ -0,0 +1,45 @@
import nodeTypeIndex from './parseTree/index.js';
import collate from '/imports/api/engine/computation/utility/collate.js';
import Context from './ResolveContext.js';
// Takes a parse ndoe and computes it to a set detail level
// returns {result, context}
export default function resolve(fn, node, scope, context = new Context()){
let type = nodeTypeIndex[node.type];
if (!type){
throw new Meteor.Error(`Parse node type: ${node.type} not implemented`);
}
if (type.resolve){
return type.resolve(fn, node, scope, context);
} else if (type[fn]) {
return type[fn](node, scope, context);
} else if (fn === 'reduce' && type.roll) {
return type.roll(node, scope, context)
} else if (type.compile){
return type.compile(node, scope, context)
} else {
throw new Meteor.Error('Compile not implemented on ' + node.type);
}
}
export function toString(node){
let type = nodeTypeIndex[node.type];
if (!type.toString){
throw new Meteor.Error('toString not implemented on ' + node.type);
}
return type.toString(node);
}
export function traverse(node, fn){
let type = nodeTypeIndex[node.type];
if (type.traverse){
return type.traverse(node, fn);
}
return fn(node);
}
export function mergeResolvedNodes(main, other){
main.errors = collate(main.errors, other.errors);
main.rolls = collate(main.rolls, other.rolls);
return main;
}