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:
12
app/imports/api/engine/computation/utility/collate.js
Normal file
12
app/imports/api/engine/computation/utility/collate.js
Normal 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;
|
||||
}
|
||||
20
app/imports/parser/ResolveContext.js
Normal file
20
app/imports/parser/ResolveContext.js
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
// Takes a parse tree and computes it down as far as possible into a real number
|
||||
@@ -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('.')}`;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
5
app/imports/parser/parseTree/_index.js
Normal file
5
app/imports/parser/parseTree/_index.js
Normal file
@@ -0,0 +1,5 @@
|
||||
import accessor from './accessor.js';
|
||||
|
||||
export default Object.freeze({
|
||||
accessor,
|
||||
});
|
||||
66
app/imports/parser/parseTree/accessor.js
Normal file
66
app/imports/parser/parseTree/accessor.js
Normal 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;
|
||||
46
app/imports/parser/parseTree/array.js
Normal file
46
app/imports/parser/parseTree/array.js
Normal 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;
|
||||
146
app/imports/parser/parseTree/call.js
Normal file
146
app/imports/parser/parseTree/call.js
Normal 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;
|
||||
18
app/imports/parser/parseTree/constant.js
Normal file
18
app/imports/parser/parseTree/constant.js
Normal 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;
|
||||
17
app/imports/parser/parseTree/error.js
Normal file
17
app/imports/parser/parseTree/error.js
Normal 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;
|
||||
59
app/imports/parser/parseTree/if.js
Normal file
59
app/imports/parser/parseTree/if.js
Normal 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;
|
||||
76
app/imports/parser/parseTree/index.js
Normal file
76
app/imports/parser/parseTree/index.js
Normal 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;
|
||||
11
app/imports/parser/parser.test.js
Normal file
11
app/imports/parser/parser.test.js
Normal 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');
|
||||
});
|
||||
});
|
||||
45
app/imports/parser/resolve.js
Normal file
45
app/imports/parser/resolve.js
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user