Moved parser to typescript

This commit is contained in:
Thaum Rystra
2024-02-18 22:45:54 +02:00
parent 5b7d352323
commit c721374278
35 changed files with 1073 additions and 449 deletions

View File

@@ -0,0 +1,28 @@
import { ParseNode } from '/imports/parser/parser';
import { ResolvedResult, Context } from '/imports/parser/resolve';
export type ResolveLevel = 'compile' | 'roll' | 'reduce';
export default interface NodeFactory {
create(node: Partial<ParseNode>): ParseNode;
compile?(
node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?(
node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
reduce?(
node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
resolve?(
fn: ResolveLevel, node: ParseNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: ParseNode): string;
traverse?(node: ParseNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map?(node: ParseNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}

View File

@@ -0,0 +1,29 @@
import { AccessorNode } from './accessor';
import { ArrayNode } from './array';
import { CallNode } from './call';
import { ConstantNode } from './constant';
import { ErrorNode } from './error';
import { IfNode } from './if';
import { IndexNode } from './indexNode';
import { NotNode } from './not';
import { OperatorNode } from './operator';
import { ParenthesisNode } from './parenthesis';
import { RollNode } from './roll';
import { RollArrayNode } from './rollArray';
import { UnaryOperatorNode } from './unaryOperator';
type ParseNode = AccessorNode
| ArrayNode
| CallNode
| ConstantNode
| ErrorNode
| IfNode
| IndexNode
| NotNode
| OperatorNode
| ParenthesisNode
| RollNode
| RollArrayNode
| UnaryOperatorNode
export default ParseNode;

View File

@@ -1,18 +1,19 @@
import accessor from './accessor';
import array from './array.js';
import array from './array';
import call from './call';
import constant from './constant';
import error from './error';
import ifNode from './if';
import index from './index';
import index from './indexNode';
import not from './not';
import operator from './operator';
import parenthesis from './parenthesis';
import roll from './roll';
import rollArray from './rollArray';
import unaryOperator from './unaryOperator';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
export default {
const factories: Record<string, NodeFactory> = {
accessor,
array,
call,
@@ -29,3 +30,5 @@ export default {
symbol: accessor,
unaryOperator,
};
export default factories;

View File

@@ -1,17 +1,41 @@
import constant from './constant';
import array from './array';
import resolve from '../resolve';
import resolve, { Context, ResolvedResult } from '/imports/parser/resolve';
import { getFromScope } from '/imports/api/creature/creatures/CreatureVariables';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
const accessor = {
create({ name, path }) {
export type AccessorNode = {
parseType: 'accessor';
path?: string[];
name: string;
}
interface AccessorFactory extends NodeFactory {
create(node: Partial<AccessorNode>): AccessorNode;
compile(
node: AccessorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
resolve?: undefined;
reduce(
node: AccessorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: AccessorNode): string;
traverse?: undefined;
map?: undefined;
}
const accessor: AccessorFactory = {
create({ name, path }: { name: string, path?: string[] }): AccessorNode {
return {
parseType: 'accessor',
path,
name,
};
},
compile(node, scope, context) {
compile(
node: AccessorNode, scope: Record<string, any>, context: Context
): ResolvedResult {
let value = getFromScope(node.name, scope);
// Get the value from the given path
node.path?.forEach(name => {
@@ -78,7 +102,7 @@ const accessor = {
context,
};
},
reduce(node, scope, context) {
reduce(node, scope, context): ResolvedResult {
let { result } = accessor.compile(node, scope, context);
({ result } = resolve('reduce', result, scope, context));
if (result.parseType === 'accessor') {

View File

@@ -1,55 +0,0 @@
import resolve, { toString, traverse, map } from '../resolve';
import constant from './constant';
const array = {
create({ values }) {
return {
parseType: '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 {
// Gracefully create an empty spot in the array for unsupported types
return undefined;
// throw `Unexpected type in constant array: ${valueType}`
}
});
return array.create({ values });
},
resolve(fn, node, scope, context) {
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));
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.values = node.values.map(value => map(value, fn));
}
return resultingNode;
},
}
export default array;

View File

@@ -0,0 +1,75 @@
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import constant from './constant';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
export type ArrayNode = {
parseType: 'array';
values: ParseNode[];
}
interface ArrayFactory extends NodeFactory {
create(node: Partial<ArrayNode>): ArrayNode;
fromConstantArray(array: (string | number | boolean | undefined)[]): ArrayNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: ArrayNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: ArrayNode): string;
traverse(node: ArrayNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: ArrayNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const array: ArrayFactory = {
create({ values }: { values: ParseNode[] }) {
return {
parseType: 'array',
values,
};
},
fromConstantArray(constantArray) {
const values = constantArray.map(value => {
const valueType = typeof value;
if (
valueType === 'string' ||
valueType === 'number' ||
valueType === 'boolean' ||
valueType === 'undefined'
) {
return constant.create({ value });
} else {
// Gracefully create an empty constant in the array for unsupported types
return constant.create({ value: undefined });
}
});
return array.create({ values });
},
resolve(fn, node, scope, context): ResolvedResult {
const values = node.values.map(node => {
const { 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));
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.values = node.values.map(value => map(value, fn));
}
return resultingNode;
},
}
export default array;

View File

@@ -1,18 +1,38 @@
import error from './error';
import constant from './constant';
import functions from '/imports/parser/functions';
import resolve, { toString, traverse, map } from '../resolve';
import functions, { ParserFunction } from '/imports/parser/functions';
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
const call = {
create({ functionName, args }) {
export type CallNode = {
parseType: 'call';
functionName: string;
args: ParseNode[];
}
interface CallFactory extends NodeFactory {
create(node: Partial<CallNode>): CallNode;
resolve(
fn: ResolveLevel, node: CallNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: CallNode): string;
traverse(node: CallNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: CallNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
checkArguments(node: CallNode, fn: ResolveLevel, func: ParserFunction,
resolvedArgs: ParseNode[], context: Context): boolean;
}
const call: CallFactory = {
create({ functionName, args = [] }: { functionName: string, args: ParseNode[] }): CallNode {
return {
parseType: 'call',
functionName,
args,
}
},
resolve(fn, node, scope, context) {
let func = functions[node.functionName];
resolve(fn, node, scope, context): ResolvedResult {
const func = functions[node.functionName];
// Check that the function exists
if (!func) {
context.error(`${node.functionName} is not a supported function`);
@@ -26,9 +46,9 @@ const call = {
}
// Resolve a given node to a maximum depth of resolution
const resolveToLevel = (node, maxResolveFn = 'reduce') => {
const resolveToLevel = (node, maxResolveFn = 'reduce'): ResolvedResult => {
// Determine the actual depth to resolve to
let resolveFn = 'reduce';
let resolveFn: ResolveLevel = 'reduce';
if (fn === 'compile' || maxResolveFn === 'compile') {
resolveFn = 'compile';
} else if (fn === 'roll' || maxResolveFn === 'roll') {
@@ -39,19 +59,13 @@ const call = {
}
// Resolve the arguments
let resolvedArgs = node.args.map((arg, i) => {
let { result } = resolveToLevel(arg, func.maxResolveLevels?.[i]);
const resolvedArgs = node.args.map((arg, i) => {
const { result } = resolveToLevel(arg, func.maxResolveLevels?.[i]);
return result;
});
// Check that the arguments match what is expected
let checkFailed = call.checkArugments({
node,
fn,
resolvedArgs,
func,
context,
});
const checkFailed = call.checkArguments(node, fn, func, resolvedArgs, context);
if (checkFailed) {
if (fn === 'reduce') {
@@ -74,8 +88,8 @@ const call = {
}
}
// Map contant nodes to constants before attempting to run the function
let mappedArgs = resolvedArgs.map((arg, index) => {
// Map constant nodes to constants before attempting to run the function
const mappedArgs = resolvedArgs.map((arg, index) => {
if (
arg.parseType === 'constant' &&
func.arguments[index] !== 'parseNode'
@@ -88,12 +102,12 @@ const call = {
try {
// Run the function
let value = func.fn.apply({
const value = func.fn.apply({
scope,
context,
}, mappedArgs);
let valueType = typeof value;
const valueType = typeof value;
if (valueType === 'number' || valueType === 'string' || valueType === 'boolean') {
// Convert constant results into constant nodes
return {
@@ -129,7 +143,7 @@ const call = {
}
return resultingNode;
},
checkArugments({ node, fn, func, resolvedArgs, context }) {
checkArguments(node, fn, func, resolvedArgs, context) {
const argumentsExpected = func.arguments;
// Check that the number of arguments matches the number expected
if (
@@ -146,7 +160,7 @@ const call = {
let failed = false;
// Check that each argument is of the correct type
resolvedArgs.forEach((node, index) => {
let type;
let type: string;
if (argumentsExpected.anyLength) {
type = argumentsExpected[0];
} else {
@@ -155,8 +169,8 @@ const call = {
if (type === 'parseNode') return;
if (node.parseType !== type && node.valueType !== type) failed = true;
if (failed && fn === 'reduce') {
let typeName = typeof type === 'string' ? type : type.constructor.name;
let nodeName = node.parseType;
const typeName = typeof type === 'string' ? type : type.constructor.name;
const nodeName = node.parseType;
context.error(`Incorrect arguments to ${node.functionName} function` +
`expected ${typeName} got ${nodeName}`);
}

View File

@@ -1,17 +0,0 @@
const constant = {
create({value}){
return {
parseType: 'constant',
valueType: typeof value,
value,
}
},
compile(node, scope, context){
return {result: node, context};
},
toString(node){
return `${node.value}`;
},
}
export default constant;

View File

@@ -0,0 +1,42 @@
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import { Context, ResolvedResult } from '/imports/parser/resolve';
type ConstantValueType = number | string | boolean | undefined
export type ConstantNode = {
parseType: 'constant';
value: ConstantValueType;
// TODO replace all `constantNode.valueType` with `typeof constantNode.value`
valueType: 'number' | 'string' | 'boolean' | 'undefined';
}
interface ConstantFactory extends NodeFactory {
create({ value }: { value: ConstantValueType }): ConstantNode;
compile(
node: ConstantNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
reduce?: undefined;
resolve?: undefined;
toString(node: ConstantNode): string;
traverse?: undefined;
map?: undefined;
}
const constant: ConstantFactory = {
create({ value }): ConstantNode {
return {
parseType: 'constant',
valueType: typeof value as 'number' | 'string' | 'boolean' | 'undefined',
value,
}
},
compile(node, scope, context) {
return { result: node, context };
},
toString(node) {
return `${node.value}`;
},
}
export default constant;

View File

@@ -1,17 +0,0 @@
const error = {
create({ node, error }) {
return {
parseType: 'error',
node,
error,
}
},
compile(node, scope, context) {
return { result: node, context };
},
toString(node) {
return `${node.error.type} error: ${node.error.message}`;
},
}
export default error;

View File

@@ -0,0 +1,40 @@
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import { Context, ResolvedResult } from '/imports/parser/resolve';
export type ErrorNode = {
parseType: 'error';
node: ParseNode;
error: string;
}
interface ErrorFactory extends NodeFactory {
create(node: Partial<ErrorNode>): ErrorNode;
compile(
node: ErrorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
reduce?: undefined;
resolve?: undefined;
toString(node: ErrorNode): string;
traverse?: undefined;
map?: undefined;
}
const error: ErrorFactory = {
create({ node, error }: { node: ParseNode, error: string }) {
return {
parseType: 'error',
node,
error,
}
},
compile(node, scope, context) {
return { result: node, context };
},
toString(node) {
return node.error;
},
}
export default error;

View File

@@ -1,53 +0,0 @@
import resolve, { traverse, toString, map } from '../resolve';
const ifNode = {
create({condition, consequent, alternative}){
return {
parseType: 'if',
condition,
consequent,
alternative,
};
},
toString(node){
let {condition, consequent, alternative} = node;
return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}`
},
resolve(fn, node, scope, context){
let {result: condition} = resolve(fn, node.condition, scope, context);
if (condition.parseType === 'constant'){
if (condition.value){
return resolve(fn, node.consequent, scope, context);
} else {
return resolve(fn, node.alternative, scope, context);
}
} else {
return {
result: ifNode.create({
condition: condition,
consequent: node.consequent,
alternative: node.alternative,
}),
context,
};
}
},
traverse(node, fn){
fn(node);
traverse(node.condition, fn);
traverse(node.consequent, fn);
traverse(node.alternative, fn);
},
map(node, fn){
const resultingNode = fn(node);
if (resultingNode === node){
node.condition = map(node.condition, fn);
node.consequent = map(node.consequent, fn);
node.alternative = map(node.alternative, fn);
}
return resultingNode;
},
}
export default ifNode;

View File

@@ -0,0 +1,79 @@
import resolve, { traverse, toString, map } from '../resolve';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import { Context, ResolvedResult } from '/imports/parser/resolve';
export type IfNode = {
parseType: 'if';
condition: ParseNode;
consequent: ParseNode;
alternative: ParseNode;
}
interface IfFactory extends NodeFactory {
create(node: Partial<IfNode>): IfNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: IfNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: IfNode): string;
traverse(node: IfNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: IfNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const ifNode: IfFactory = {
create(
{ condition, consequent, alternative }:
{ condition: ParseNode, consequent: ParseNode, alternative: ParseNode }
) {
return {
parseType: 'if',
condition,
consequent,
alternative,
};
},
toString(node) {
const { condition, consequent, alternative } = node;
condition.parseType
return `${toString(condition)} ? ${toString(consequent)} : ${toString(alternative)}`
},
resolve(fn, node, scope, context): ResolvedResult {
const { result: condition } = resolve(fn, node.condition, scope, context);
if (condition.parseType === 'constant') {
if (condition.value) {
return resolve(fn, node.consequent, scope, context);
} else {
return resolve(fn, node.alternative, scope, context);
}
} else {
return {
result: ifNode.create({
condition: condition,
consequent: node.consequent,
alternative: node.alternative,
}),
context,
};
}
},
traverse(node, fn) {
fn(node);
traverse(node.condition, fn);
traverse(node.consequent, fn);
traverse(node.alternative, fn);
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.condition = map(node.condition, fn);
node.consequent = map(node.consequent, fn);
node.alternative = map(node.alternative, fn);
}
return resultingNode;
},
}
export default ifNode;

View File

@@ -1,8 +1,29 @@
import resolve, { traverse, toString, map } from '../resolve';
import resolve, { traverse, toString, map, ResolvedResult, Context } from '/imports/parser/resolve';
import error from './error';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
const indexNode = {
create({ array, index }) {
export type IndexNode = {
parseType: 'index';
array: ParseNode;
index: ParseNode;
}
interface IndexFactory extends NodeFactory {
create(node: Partial<IndexNode>): IndexNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: IndexNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: IndexNode): string;
traverse(node: IndexNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: IndexNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const indexNode: IndexFactory = {
create({ array, index }: { array: ParseNode, index: ParseNode }) {
return {
parseType: 'index',
array,
@@ -10,8 +31,8 @@ const indexNode = {
}
},
resolve(fn, node, scope, context) {
let { result: index } = resolve(fn, node.index, scope, context);
let { result: array } = resolve(fn, node.array, scope, context);
const { result: index } = resolve(fn, node.index, scope, context);
const { result: array } = resolve(fn, node.array, scope, context);
if (
index.valueType === 'number' &&
@@ -25,7 +46,7 @@ const indexNode = {
` of length ${array.values.length}`,
});
}
let selection = array.values[index.value - 1];
const selection = array.values[index.value - 1];
if (selection) {
return resolve(fn, selection, scope, context);
}
@@ -63,12 +84,12 @@ const indexNode = {
toString(node) {
return `${toString(node.array)}[${toString(node.index)}]`;
},
traverse(node, fn) {
traverse(node, fn: (node: ParseNode) => any) {
fn(node);
traverse(node.array, fn);
traverse(node.index, fn);
},
map(node, fn) {
map(node, fn: (node: ParseNode) => any) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.array = map(node.array, fn);

View File

@@ -1,44 +0,0 @@
import resolve, { toString, traverse, map } from '../resolve';
import constant from './constant';
const not = {
create({ right }) {
return {
parseType: 'not',
right,
}
},
resolve(fn, node, scope, context) {
const { result: right } = resolve(fn, node.right, scope, context);
if (right.parseType !== 'constant') {
return {
result: not.create({
right: right,
}),
context,
};
}
return {
result: constant.create({
value: !right.value,
}),
context,
};
},
toString(node) {
return `!${toString(node.right)}`;
},
traverse(node, fn) {
fn(node);
traverse(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.right = map(node.right, fn);
}
return resultingNode;
},
}
export default not;

View File

@@ -0,0 +1,64 @@
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import constant from './constant';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
export type NotNode = {
parseType: 'not';
right: ParseNode;
}
interface NotFactory extends NodeFactory {
create(node: Partial<NotNode>): NotNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: NotNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: NotNode): string;
traverse(node: NotNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: NotNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const not: NotFactory = {
create({ right }: { right: ParseNode }) {
return {
parseType: 'not',
right,
}
},
resolve(fn, node, scope, context) {
const { result: right } = resolve(fn, node.right, scope, context);
if (right.parseType !== 'constant') {
return {
result: not.create({
right: right,
}),
context,
};
}
return {
result: constant.create({
value: !right.value,
}),
context,
};
},
toString(node) {
return `!${toString(node.right)}`;
},
traverse(node, fn) {
fn(node);
traverse(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.right = map(node.right, fn);
}
return resultingNode;
},
}
export default not;

View File

@@ -1,12 +1,41 @@
import resolve, { toString, traverse, map } from '../resolve';
import resolve, { toString, traverse, map, ResolvedResult, Context } from '/imports/parser/resolve';
import constant from './constant';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
type OperatorSymbol = '*' | '/' | '^' | '+' | '-' | '%' | '&' | '&&' | '|' | '||' | '=' |
'==' | '===' | '!=' | '!==' | '>' | '<' | '>=' | '<=';
export type OperatorNode = {
parseType: 'operator';
left: ParseNode;
right: ParseNode;
operator: OperatorSymbol;
}
interface OperatorFactory extends NodeFactory {
create(node: Partial<OperatorNode>): OperatorNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: OperatorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: OperatorNode): string;
traverse(node: OperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: OperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
// Which operators can be considered commutative by the parser
// i.e. 1 + 2 + 3 === 2 + 3 + 1
const commutativeOperators = ['+', '*']
const operator = {
create({ left, right, operator }) {
const operator: OperatorFactory = {
create({
left, right, operator
}: {
left: ParseNode, right: ParseNode, operator: OperatorSymbol
}) {
return {
parseType: 'operator',
left,
@@ -45,7 +74,7 @@ const operator = {
};
},
toString(node) {
let { left, right, operator } = node;
const { left, right, operator } = node;
// special case of adding a negative number
if (operator === '+' && right.valueType === 'number' && right.value < 0) {
return `${toString(left)} - ${-right.value}`
@@ -67,7 +96,7 @@ const operator = {
},
}
function applyOperator(operator, left, right) {
function applyOperator(operator: OperatorSymbol, left: ParseNode, right: ParseNode) {
let result;
switch (operator) {
case '+': result = left + right; break;
@@ -93,7 +122,9 @@ function applyOperator(operator, left, right) {
return result;
}
function reorderCommutativeOperations(node, leftNode, rightNode) {
function reorderCommutativeOperations(
node: OperatorNode, leftNode: ParseNode, rightNode: ParseNode
) {
// Make sure the operator is commutative
if (!commutativeOperators.includes(node.operator)) return;

View File

@@ -1,41 +0,0 @@
import resolve, { toString, traverse, map } from '../resolve';
const parenthesis = {
create({ content }) {
return {
parseType: 'parenthesis',
content,
};
},
resolve(fn, node, scope, context) {
const { result: content } = resolve(fn, node.content, scope, context);
if (
fn === 'reduce' ||
content.parseType === 'constant' ||
content.parseType === 'error'
) {
return { result: content, context };
} else {
return {
result: parenthesis.create({ content }),
context
};
}
},
toString(node) {
return `(${toString(node.content)})`;
},
traverse(node, fn) {
fn(node);
traverse(node.content, fn);
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.content = map(node.content, fn);
}
return resultingNode;
},
}
export default parenthesis;

View File

@@ -0,0 +1,61 @@
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
export type ParenthesisNode = {
parseType: 'parenthesis';
content: ParseNode;
}
interface ParenthesisFactory extends NodeFactory {
create(node: Partial<ParenthesisNode>): ParenthesisNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: ParenthesisNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: ParenthesisNode): string;
traverse(node: ParenthesisNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: ParenthesisNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const parenthesis: ParenthesisFactory = {
create({ content }: { content: ParseNode }) {
return {
parseType: 'parenthesis',
content,
};
},
resolve(fn, node, scope, context) {
const { result: content } = resolve(fn, node.content, scope, context);
if (
fn === 'reduce' ||
content.parseType === 'constant' ||
content.parseType === 'error'
) {
return { result: content, context };
} else {
return {
result: parenthesis.create({ content }),
context
};
}
},
toString(node) {
return `(${toString(node.content)})`;
},
traverse(node, fn: (node: ParseNode) => any) {
fn(node);
traverse(node.content, fn);
},
map(node, fn: (node: ParseNode) => any) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.content = map(node.content, fn);
}
return resultingNode;
},
}
export default parenthesis;

View File

@@ -1,11 +1,36 @@
import resolve, { toString, traverse, map } from '../resolve';
import resolve, { toString, traverse, map, ResolvedResult, Context } from '../resolve';
import error from './error';
import rollArray from './rollArray';
import rollDice from '/imports/parser/rollDice';
import STORAGE_LIMITS from '/imports/constants/STORAGE_LIMITS';
import ParseNode from '/imports/parser/parseTree/ParseNode';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
const rollNode = {
create({ left, right }) {
export type RollNode = {
parseType: 'roll';
left: ParseNode;
right: ParseNode;
}
interface RollNodeFactory extends NodeFactory {
create(node: Partial<RollNode>): RollNode;
compile(
node: RollNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll(
node: RollNode, scope: Record<string, any>, context: Context
): ResolvedResult;
reduce(
node: RollNode, scope: Record<string, any>, context: Context
): ResolvedResult;
resolve?: undefined;
toString(node: RollNode): string;
traverse(node: RollNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: RollNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const rollNode: RollNodeFactory = {
create({ left, right }: { left: ParseNode, right: ParseNode }) {
return {
parseType: 'roll',
left,
@@ -22,7 +47,9 @@ const rollNode = {
},
toString(node) {
if (
node.left.valueType === 'number' && node.left.value === 1
node.left.parseType === 'constant'
&& typeof node.left.value === 'number'
&& node.left.value === 1
) {
return `d${toString(node.right)}`;
} else {
@@ -32,10 +59,18 @@ const rollNode = {
roll(node, scope, context) {
const { result: left } = resolve('reduce', node.left, scope, context);
const { result: right } = resolve('reduce', node.right, scope, context);
if (left.valueType !== 'number' && !Number.isInteger(left.value)) {
if (
left.parseType !== 'constant'
|| typeof left.value !== 'number'
|| !Number.isInteger(left.value)
) {
return errorResult('Number of dice is not an integer', node, context);
}
if (right.valueType !== 'number' && !Number.isInteger(right.value)) {
if (
right.parseType !== 'constant'
|| typeof right.value !== 'number'
|| !Number.isInteger(right.value)
) {
return errorResult('Dice size is not an integer', node, context);
}
let number = left.value;
@@ -46,8 +81,8 @@ const rollNode = {
const message = `Can't roll more than ${STORAGE_LIMITS.diceRollValuesCount} dice at once`;
return errorResult(message, node, context);
}
let diceSize = right.value;
let values = rollDice(number, diceSize);
const diceSize = right.value;
const values = rollDice(number, diceSize);
if (context) {
context.rolls.push({ number, diceSize, values });
}
@@ -79,7 +114,7 @@ const rollNode = {
},
}
function errorResult(message, node, context) {
function errorResult(message: string, node: RollNode, context: Context) {
context.error(message);
return {
result: error.create({ node, error: message }),

View File

@@ -1,46 +0,0 @@
import constant from './constant';
const rollArray = {
create({ values, diceSize, diceNum }) {
return {
parseType: 'rollArray',
values: values.map(v => ({ value: v })),
diceSize,
diceNum,
};
},
compile(node, scope, context) {
return {
result: node,
context
};
},
toString(node) {
return `${node.diceNum || ''}d${node.diceSize} [${valuesToString(node.values)}]`;
},
reduce(node, scope, context) {
const total = node.values.reduce((a, b) => {
if (b.disabled) return a;
return a + b.value;
}, 0);
return {
result: constant.create({
value: total,
}),
context,
};
},
}
function valuesToString(values) {
return values.map(v => {
let text = `${v.value}`;
if (v.disabled) text = `~~${text}~~`;
if (v.italics) text = `*${text}*`;
if (v.bold) text = `**${text}**`;
if (v.underline) text = `__${text}__`;
return text;
}).join(', ');
}
export default rollArray;

View File

@@ -0,0 +1,80 @@
import constant from './constant';
import NodeFactory from '/imports/parser/parseTree/NodeFactory';
import { Context, ResolvedResult } from '/imports/parser/resolve';
type RollValue = {
value: number,
disabled?: true,
italics?: true,
bold?: true,
underline?: true,
}
export type RollArrayNode = {
parseType: 'rollArray';
values: RollValue[];
diceSize: number,
diceNum: number,
}
interface RollArrayFactory extends NodeFactory {
create(input: { values: number[], diceSize: number, diceNum: number }): RollArrayNode;
compile(
node: RollArrayNode, scope: Record<string, any>, context: Context
): ResolvedResult;
roll?: undefined;
reduce(
node: RollArrayNode, scope: Record<string, any>, context: Context
): ResolvedResult;
resolve?: undefined;
toString(node: RollArrayNode): string;
traverse?: undefined;
map?: undefined;
}
const rollArray: RollArrayFactory = {
create({
values, diceSize, diceNum
}) {
return {
parseType: 'rollArray',
values: values.map(v => ({ value: v })),
diceSize,
diceNum,
};
},
compile(node, scope, context) {
return {
result: node,
context
};
},
toString(node) {
return `${node.diceNum || ''}d${node.diceSize} [${valuesToString(node.values)}]`;
},
reduce(node, scope, context) {
const total = node.values.reduce((a, b) => {
if (b.disabled) return a;
return a + b.value;
}, 0);
return {
result: constant.create({
value: total,
}),
context,
};
},
}
function valuesToString(values: RollValue[]) {
return values.map(v => {
let text = `${v.value}`;
if (v.disabled) text = `~~${text}~~`;
if (v.italics) text = `*${text}*`;
if (v.bold) text = `**${text}**`;
if (v.underline) text = `__${text}__`;
return text;
}).join(', ');
}
export default rollArray;

View File

@@ -1,53 +0,0 @@
import resolve, { toString, traverse, map } from '../resolve';
import constant from './constant';
const unaryOperator = {
create({ operator, right }) {
return {
parseType: 'unaryOperator',
operator,
right,
};
},
resolve(fn, node, scope, context) {
const { result: rightNode } = resolve(fn, node.right, scope, context);
if (rightNode.valueType !== 'number') {
return {
result: unaryOperator.create({
operator: node.operator,
right: rightNode,
}),
context,
};
}
let right = rightNode.value;
let result;
switch (node.operator) {
case '-': result = -right; break;
case '+': result = +right; break;
}
return {
result: constant.create({
value: result,
parseType: typeof result,
}),
context,
};
},
toString(node) {
return `${node.operator}${toString(node.right)}`;
},
traverse(node, fn) {
fn(node);
traverse(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.right = map(node.right, fn);
}
return resultingNode;
},
};
export default unaryOperator;

View File

@@ -0,0 +1,78 @@
import resolve, { toString, traverse, map, Context, ResolvedResult } from '/imports/parser/resolve';
import constant from './constant';
import NodeFactory, { ResolveLevel } from '/imports/parser/parseTree/NodeFactory';
import ParseNode from '/imports/parser/parseTree/ParseNode';
type UnaryOperatorSymbol = '+' | '-';
export type UnaryOperatorNode = {
parseType: 'unaryOperator';
operator: UnaryOperatorSymbol;
right: ParseNode;
}
interface UnaryOperatorFactory extends NodeFactory {
create(node: Partial<UnaryOperatorNode>): UnaryOperatorNode;
compile?: undefined;
roll?: undefined;
reduce?: undefined;
resolve(
fn: ResolveLevel, node: UnaryOperatorNode, scope: Record<string, any>, context: Context
): ResolvedResult;
toString(node: UnaryOperatorNode): string;
traverse(node: UnaryOperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
map(node: UnaryOperatorNode, fn: (node: ParseNode) => any): ReturnType<typeof fn>;
}
const unaryOperator: UnaryOperatorFactory = {
create({ operator, right }: { operator: UnaryOperatorSymbol, right: ParseNode }) {
return {
parseType: 'unaryOperator',
operator,
right,
};
},
resolve(fn, node, scope, context) {
const { result: rightNode } = resolve(fn, node.right, scope, context);
if (
rightNode.parseType !== 'constant'
|| typeof rightNode.value !== 'number'
) {
return {
result: unaryOperator.create({
operator: node.operator,
right: rightNode,
}),
context,
};
}
const right = rightNode.value;
let result: number;
switch (node.operator) {
case '-': result = -right; break;
case '+': result = +right; break;
}
return {
result: constant.create({
value: result,
}),
context,
};
},
toString(node) {
return `${node.operator}${toString(node.right)}`;
},
traverse(node, fn) {
fn(node);
traverse(node.right, fn);
},
map(node, fn) {
const resultingNode = fn(node);
if (resultingNode === node) {
node.right = map(node.right, fn);
}
return resultingNode;
},
};
export default unaryOperator;