From c721374278f9c065be10de86e8172fce1a2cf93f Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Sun, 18 Feb 2024 22:45:54 +0200 Subject: [PATCH] Moved parser to typescript --- .vscode/settings.json | 4 +- .../parser/{functions.js => functions.ts} | 31 +++-- app/imports/parser/parseTree/NodeFactory.ts | 28 +++++ app/imports/parser/parseTree/ParseNode.ts | 29 +++++ .../parser/parseTree/{_index.js => _index.ts} | 9 +- .../parseTree/{accessor.js => accessor.ts} | 34 +++++- app/imports/parser/parseTree/array.js | 55 --------- app/imports/parser/parseTree/array.ts | 75 ++++++++++++ .../parser/parseTree/{call.js => call.ts} | 64 +++++++---- app/imports/parser/parseTree/constant.js | 17 --- app/imports/parser/parseTree/constant.ts | 42 +++++++ app/imports/parser/parseTree/error.js | 17 --- app/imports/parser/parseTree/error.ts | 40 +++++++ app/imports/parser/parseTree/if.js | 53 --------- app/imports/parser/parseTree/if.ts | 79 +++++++++++++ .../parseTree/{index.js => indexNode.ts} | 37 ++++-- app/imports/parser/parseTree/not.js | 44 ------- app/imports/parser/parseTree/not.ts | 64 +++++++++++ .../parseTree/{operator.js => operator.ts} | 43 ++++++- app/imports/parser/parseTree/parenthesis.js | 41 ------- app/imports/parser/parseTree/parenthesis.ts | 61 ++++++++++ .../parser/parseTree/{roll.js => roll.ts} | 53 +++++++-- app/imports/parser/parseTree/rollArray.js | 46 -------- app/imports/parser/parseTree/rollArray.ts | 80 +++++++++++++ app/imports/parser/parseTree/unaryOperator.js | 53 --------- app/imports/parser/parseTree/unaryOperator.ts | 78 +++++++++++++ app/imports/parser/parser.js | 34 ------ app/imports/parser/parser.ts | 34 ++++++ app/imports/parser/{resolve.js => resolve.ts} | 61 ++++++---- app/package.json | 3 +- node_modules/@types/nearley/LICENSE | 21 ++++ node_modules/@types/nearley/README.md | 15 +++ node_modules/@types/nearley/index.d.ts | 108 ++++++++++++++++++ node_modules/@types/nearley/package.json | 58 ++++++++++ package-lock.json | 11 ++ 35 files changed, 1073 insertions(+), 449 deletions(-) rename app/imports/parser/{functions.js => functions.ts} (90%) create mode 100644 app/imports/parser/parseTree/NodeFactory.ts create mode 100644 app/imports/parser/parseTree/ParseNode.ts rename app/imports/parser/parseTree/{_index.js => _index.ts} (75%) rename app/imports/parser/parseTree/{accessor.js => accessor.ts} (74%) delete mode 100644 app/imports/parser/parseTree/array.js create mode 100644 app/imports/parser/parseTree/array.ts rename app/imports/parser/parseTree/{call.js => call.ts} (66%) delete mode 100644 app/imports/parser/parseTree/constant.js create mode 100644 app/imports/parser/parseTree/constant.ts delete mode 100644 app/imports/parser/parseTree/error.js create mode 100644 app/imports/parser/parseTree/error.ts delete mode 100644 app/imports/parser/parseTree/if.js create mode 100644 app/imports/parser/parseTree/if.ts rename app/imports/parser/parseTree/{index.js => indexNode.ts} (59%) delete mode 100644 app/imports/parser/parseTree/not.js create mode 100644 app/imports/parser/parseTree/not.ts rename app/imports/parser/parseTree/{operator.js => operator.ts} (74%) delete mode 100644 app/imports/parser/parseTree/parenthesis.js create mode 100644 app/imports/parser/parseTree/parenthesis.ts rename app/imports/parser/parseTree/{roll.js => roll.ts} (58%) delete mode 100644 app/imports/parser/parseTree/rollArray.js create mode 100644 app/imports/parser/parseTree/rollArray.ts delete mode 100644 app/imports/parser/parseTree/unaryOperator.js create mode 100644 app/imports/parser/parseTree/unaryOperator.ts delete mode 100644 app/imports/parser/parser.js create mode 100644 app/imports/parser/parser.ts rename app/imports/parser/{resolve.js => resolve.ts} (50%) create mode 100644 node_modules/@types/nearley/LICENSE create mode 100644 node_modules/@types/nearley/README.md create mode 100644 node_modules/@types/nearley/index.d.ts create mode 100644 node_modules/@types/nearley/package.json create mode 100644 package-lock.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 0ecb2a82..81f51528 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,9 +1,11 @@ { "cSpell.words": [ "autorun", + "blackbox", "Crits", "cyrb", "EJSON", + "nearley", "uncomputed" ] -} \ No newline at end of file +} diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.ts similarity index 90% rename from app/imports/parser/functions.js rename to app/imports/parser/functions.ts index 3e55bbec..acb445bf 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.ts @@ -1,7 +1,19 @@ +import { ResolveLevel } from '/imports/parser/parseTree/NodeFactory'; import resolve from '/imports/parser/resolve' import rollDice from '/imports/parser/rollDice'; -export default { +export type ParserFunction = { + comment: string; + examples: { input: string, result: string }[]; + arguments: string[]; + maxResolveLevels?: ResolveLevel[]; + minArguments?: number, + maxArguments?: number, + resultType: string; + fn: (...args: any[]) => any; +} + +const parserFunctions: { [name: string]: ParserFunction } = { 'abs': { comment: 'Returns the absolute value of a number', examples: [ @@ -105,22 +117,23 @@ export default { arguments: ['array', 'number'], resultType: 'number', fn: function tableLookup(arrayNode, number) { - for (let i in arrayNode.values) { - let node = arrayNode.values[i]; + for (const i in arrayNode.values) { + const node = arrayNode.values[i]; if (node.value > number) return +i; } return arrayNode.values.length; } }, 'resolve': { - comment: 'Forces the given calcultion to resolve into a number, even in calculations where it would usually keep the unknown values as is', + comment: 'Forces the given calculation to resolve into a number, even in calculations where it would usually keep the unknown values as is', examples: [ { input: 'resolve(someUndefinedVariable + 3 + 4)', result: '7' }, { input: 'resolve(1d6)', result: '4' }, ], arguments: ['parseNode'], + resultType: 'parseNode', fn: function resolveFn(node) { - let { result } = resolve('reduce', node, this.scope, this.context); + const { result } = resolve('reduce', node, this.scope, this.context); return result; } }, @@ -181,7 +194,7 @@ export default { maxArguments: 3, resultType: 'rollArray', fn: function rerollFn(rollArray, numberToReroll = 1, keepNewRoll = false) { - let rollValues = rollArray.values + const rollValues = rollArray.values // Iterate through the roll values for (let i = 0; i < rollValues.length; i += 1) { // If the number is less than the reroll limit @@ -218,7 +231,7 @@ export default { fn: function explodeFn(rollArray, depth = 1, numberToReroll = rollArray.diceSize) { let overflowErrored = false; if (depth > 99) depth = 99; - let rollValues = rollArray.values + const rollValues = rollArray.values // Iterate through the roll values for (let i = 0; i < rollValues.length; i += 1) { // If the number is greater than or equal to the reroll limit @@ -255,7 +268,9 @@ export default { } function anyNumberOf(type) { - let argumentArray = [type]; + const argumentArray = [type]; argumentArray.anyLength = true; return argumentArray; } + +export default parserFunctions; \ No newline at end of file diff --git a/app/imports/parser/parseTree/NodeFactory.ts b/app/imports/parser/parseTree/NodeFactory.ts new file mode 100644 index 00000000..fb3d1821 --- /dev/null +++ b/app/imports/parser/parseTree/NodeFactory.ts @@ -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; + + compile?( + node: ParseNode, scope: Record, context: Context + ): ResolvedResult; + + roll?( + node: ParseNode, scope: Record, context: Context + ): ResolvedResult; + + reduce?( + node: ParseNode, scope: Record, context: Context + ): ResolvedResult; + + resolve?( + fn: ResolveLevel, node: ParseNode, scope: Record, context: Context + ): ResolvedResult; + + toString(node: ParseNode): string; + traverse?(node: ParseNode, fn: (node: ParseNode) => any): ReturnType; + map?(node: ParseNode, fn: (node: ParseNode) => any): ReturnType; +} diff --git a/app/imports/parser/parseTree/ParseNode.ts b/app/imports/parser/parseTree/ParseNode.ts new file mode 100644 index 00000000..04140f60 --- /dev/null +++ b/app/imports/parser/parseTree/ParseNode.ts @@ -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; \ No newline at end of file diff --git a/app/imports/parser/parseTree/_index.js b/app/imports/parser/parseTree/_index.ts similarity index 75% rename from app/imports/parser/parseTree/_index.js rename to app/imports/parser/parseTree/_index.ts index 345e7fa8..89dbfa96 100644 --- a/app/imports/parser/parseTree/_index.js +++ b/app/imports/parser/parseTree/_index.ts @@ -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 = { accessor, array, call, @@ -29,3 +30,5 @@ export default { symbol: accessor, unaryOperator, }; + +export default factories; diff --git a/app/imports/parser/parseTree/accessor.js b/app/imports/parser/parseTree/accessor.ts similarity index 74% rename from app/imports/parser/parseTree/accessor.js rename to app/imports/parser/parseTree/accessor.ts index 1a4240c8..ee72779e 100644 --- a/app/imports/parser/parseTree/accessor.js +++ b/app/imports/parser/parseTree/accessor.ts @@ -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; + compile( + node: AccessorNode, scope: Record, context: Context + ): ResolvedResult; + roll?: undefined; + resolve?: undefined; + reduce( + node: AccessorNode, scope: Record, 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, 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') { diff --git a/app/imports/parser/parseTree/array.js b/app/imports/parser/parseTree/array.js deleted file mode 100644 index 8ea90b12..00000000 --- a/app/imports/parser/parseTree/array.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/array.ts b/app/imports/parser/parseTree/array.ts new file mode 100644 index 00000000..c37b5d1a --- /dev/null +++ b/app/imports/parser/parseTree/array.ts @@ -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; + fromConstantArray(array: (string | number | boolean | undefined)[]): ArrayNode; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: ArrayNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: ArrayNode): string; + traverse(node: ArrayNode, fn: (node: ParseNode) => any): ReturnType; + map(node: ArrayNode, fn: (node: ParseNode) => any): ReturnType; +} + +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; diff --git a/app/imports/parser/parseTree/call.js b/app/imports/parser/parseTree/call.ts similarity index 66% rename from app/imports/parser/parseTree/call.js rename to app/imports/parser/parseTree/call.ts index 1a5f2396..9712c9ad 100644 --- a/app/imports/parser/parseTree/call.js +++ b/app/imports/parser/parseTree/call.ts @@ -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; + resolve( + fn: ResolveLevel, node: CallNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: CallNode): string; + traverse(node: CallNode, fn: (node: ParseNode) => any): ReturnType; + map(node: CallNode, fn: (node: ParseNode) => any): ReturnType; + 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}`); } diff --git a/app/imports/parser/parseTree/constant.js b/app/imports/parser/parseTree/constant.js deleted file mode 100644 index e382b897..00000000 --- a/app/imports/parser/parseTree/constant.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/constant.ts b/app/imports/parser/parseTree/constant.ts new file mode 100644 index 00000000..23c2db8c --- /dev/null +++ b/app/imports/parser/parseTree/constant.ts @@ -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, 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; diff --git a/app/imports/parser/parseTree/error.js b/app/imports/parser/parseTree/error.js deleted file mode 100644 index 1692aae9..00000000 --- a/app/imports/parser/parseTree/error.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/error.ts b/app/imports/parser/parseTree/error.ts new file mode 100644 index 00000000..484d8b50 --- /dev/null +++ b/app/imports/parser/parseTree/error.ts @@ -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; + compile( + node: ErrorNode, scope: Record, 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; diff --git a/app/imports/parser/parseTree/if.js b/app/imports/parser/parseTree/if.js deleted file mode 100644 index 126b0463..00000000 --- a/app/imports/parser/parseTree/if.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/if.ts b/app/imports/parser/parseTree/if.ts new file mode 100644 index 00000000..d6a4e332 --- /dev/null +++ b/app/imports/parser/parseTree/if.ts @@ -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; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: IfNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: IfNode): string; + traverse(node: IfNode, fn: (node: ParseNode) => any): ReturnType; + map(node: IfNode, fn: (node: ParseNode) => any): ReturnType; +} + +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; diff --git a/app/imports/parser/parseTree/index.js b/app/imports/parser/parseTree/indexNode.ts similarity index 59% rename from app/imports/parser/parseTree/index.js rename to app/imports/parser/parseTree/indexNode.ts index d866cc38..a28868f9 100644 --- a/app/imports/parser/parseTree/index.js +++ b/app/imports/parser/parseTree/indexNode.ts @@ -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; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: IndexNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: IndexNode): string; + traverse(node: IndexNode, fn: (node: ParseNode) => any): ReturnType; + map(node: IndexNode, fn: (node: ParseNode) => any): ReturnType; +} + +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); diff --git a/app/imports/parser/parseTree/not.js b/app/imports/parser/parseTree/not.js deleted file mode 100644 index ff87c921..00000000 --- a/app/imports/parser/parseTree/not.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/not.ts b/app/imports/parser/parseTree/not.ts new file mode 100644 index 00000000..052e8d83 --- /dev/null +++ b/app/imports/parser/parseTree/not.ts @@ -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; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: NotNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: NotNode): string; + traverse(node: NotNode, fn: (node: ParseNode) => any): ReturnType; + map(node: NotNode, fn: (node: ParseNode) => any): ReturnType; +} + +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; diff --git a/app/imports/parser/parseTree/operator.js b/app/imports/parser/parseTree/operator.ts similarity index 74% rename from app/imports/parser/parseTree/operator.js rename to app/imports/parser/parseTree/operator.ts index 48f6d7f4..16152a9c 100644 --- a/app/imports/parser/parseTree/operator.js +++ b/app/imports/parser/parseTree/operator.ts @@ -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; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: OperatorNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: OperatorNode): string; + traverse(node: OperatorNode, fn: (node: ParseNode) => any): ReturnType; + map(node: OperatorNode, fn: (node: ParseNode) => any): ReturnType; +} // 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; diff --git a/app/imports/parser/parseTree/parenthesis.js b/app/imports/parser/parseTree/parenthesis.js deleted file mode 100644 index 38269b66..00000000 --- a/app/imports/parser/parseTree/parenthesis.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/parenthesis.ts b/app/imports/parser/parseTree/parenthesis.ts new file mode 100644 index 00000000..8c0c9e6f --- /dev/null +++ b/app/imports/parser/parseTree/parenthesis.ts @@ -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; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: ParenthesisNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: ParenthesisNode): string; + traverse(node: ParenthesisNode, fn: (node: ParseNode) => any): ReturnType; + map(node: ParenthesisNode, fn: (node: ParseNode) => any): ReturnType; +} + +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; diff --git a/app/imports/parser/parseTree/roll.js b/app/imports/parser/parseTree/roll.ts similarity index 58% rename from app/imports/parser/parseTree/roll.js rename to app/imports/parser/parseTree/roll.ts index f68c6483..405f0e02 100644 --- a/app/imports/parser/parseTree/roll.js +++ b/app/imports/parser/parseTree/roll.ts @@ -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; + compile( + node: RollNode, scope: Record, context: Context + ): ResolvedResult; + roll( + node: RollNode, scope: Record, context: Context + ): ResolvedResult; + reduce( + node: RollNode, scope: Record, context: Context + ): ResolvedResult; + resolve?: undefined; + toString(node: RollNode): string; + traverse(node: RollNode, fn: (node: ParseNode) => any): ReturnType; + map(node: RollNode, fn: (node: ParseNode) => any): ReturnType; +} + +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 }), diff --git a/app/imports/parser/parseTree/rollArray.js b/app/imports/parser/parseTree/rollArray.js deleted file mode 100644 index ecd910d8..00000000 --- a/app/imports/parser/parseTree/rollArray.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/rollArray.ts b/app/imports/parser/parseTree/rollArray.ts new file mode 100644 index 00000000..da408994 --- /dev/null +++ b/app/imports/parser/parseTree/rollArray.ts @@ -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, context: Context + ): ResolvedResult; + roll?: undefined; + reduce( + node: RollArrayNode, scope: Record, 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; diff --git a/app/imports/parser/parseTree/unaryOperator.js b/app/imports/parser/parseTree/unaryOperator.js deleted file mode 100644 index 01853ddb..00000000 --- a/app/imports/parser/parseTree/unaryOperator.js +++ /dev/null @@ -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; diff --git a/app/imports/parser/parseTree/unaryOperator.ts b/app/imports/parser/parseTree/unaryOperator.ts new file mode 100644 index 00000000..9bea236a --- /dev/null +++ b/app/imports/parser/parseTree/unaryOperator.ts @@ -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; + compile?: undefined; + roll?: undefined; + reduce?: undefined; + resolve( + fn: ResolveLevel, node: UnaryOperatorNode, scope: Record, context: Context + ): ResolvedResult; + toString(node: UnaryOperatorNode): string; + traverse(node: UnaryOperatorNode, fn: (node: ParseNode) => any): ReturnType; + map(node: UnaryOperatorNode, fn: (node: ParseNode) => any): ReturnType; +} + +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; diff --git a/app/imports/parser/parser.js b/app/imports/parser/parser.js deleted file mode 100644 index 20e36fe4..00000000 --- a/app/imports/parser/parser.js +++ /dev/null @@ -1,34 +0,0 @@ -import grammar from '/imports/parser/grammar'; -import nearley from 'nearley'; - -const nearleyGrammar = nearley.Grammar.fromCompiled(grammar); - -export default function parser() { - return new nearley.Parser(nearleyGrammar); -} - -export function parse(string) { - let parser = new nearley.Parser(nearleyGrammar); - parser.feed(string); - let results = parser.results; - if (results.length === 1) { - return results[0]; - } else if (results.length === 0) { - // Valid parsing up until now, but need more - throw new EndOfInputError('Unexpected end of input'); - } else { - console.warn('Grammar is ambiguous!', { string, results }); - return results[0]; - } -} - -export function prettifyParseError(e) { - if (e.message) e = e.message - return e.toString().split('.')[0]; -} - -class EndOfInputError extends Error { - constructor(message = '', ...args) { - super(message, ...args); - } -} diff --git a/app/imports/parser/parser.ts b/app/imports/parser/parser.ts new file mode 100644 index 00000000..192fd167 --- /dev/null +++ b/app/imports/parser/parser.ts @@ -0,0 +1,34 @@ +import grammar from '/imports/parser/grammar'; +import { Parser, Grammar } from 'nearley'; +import ParseNode from '/imports/parser/parseTree/ParseNode'; + +const nearleyGrammar = Grammar.fromCompiled(grammar); + +export default function parser() { + return new Parser(nearleyGrammar); +} + +export function parse(string: string): ParseNode { + const parser = new Parser(nearleyGrammar); + parser.feed(string); + const results = parser.results; + if (results.length === 1) { + return results[0]; + } else if (results.length === 0) { + // Valid parsing up until now, but need more + throw new EndOfInputError('Unexpected end of input'); + } else { + console.warn('Grammar is ambiguous!', { string, results }); + return results[0]; + } +} + +export function prettifyParseError(e: Meteor.Error | Error): string { + return e.message.split('.')[0]; +} + +class EndOfInputError extends Error { + constructor(message = '') { + super(message); + } +} diff --git a/app/imports/parser/resolve.js b/app/imports/parser/resolve.ts similarity index 50% rename from app/imports/parser/resolve.js rename to app/imports/parser/resolve.ts index 82bfae4d..7d25bbb7 100644 --- a/app/imports/parser/resolve.js +++ b/app/imports/parser/resolve.ts @@ -1,32 +1,36 @@ import nodeTypeIndex from './parseTree/_index'; +import ParseNode from '/imports/parser/parseTree/ParseNode'; -// Takes a parse ndoe and computes it to a set detail level +// Takes a parse node and computes it to a set detail level // returns {result, context} -export default function resolve(fn, node, scope, context = new Context()) { - if (!node) return { result: undefined, context }; - let type = nodeTypeIndex[node.parseType]; - if (!type) { +export default function resolve( + fn: 'roll' | 'reduce' | 'compile', + node: ParseNode, + scope: Record, + context = new Context() +): ResolvedResult { + if (!node) throw 'Node must be supplied'; + const factory = nodeTypeIndex[node.parseType]; + const handlerFunction = factory[fn]; + if (!factory) { throw new Meteor.Error(`Parse node type: ${node.parseType} 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) + if (factory.resolve) { + return factory.resolve(fn, node, scope, context); + } else if (handlerFunction) { + return handlerFunction(node, scope, context); + } else if (fn === 'reduce' && factory.roll) { + return factory.roll(node, scope, context) + } else if (factory.compile) { + return factory.compile(node, scope, context) } else { throw new Meteor.Error('Compile not implemented on ' + node.parseType); } } -export function toString(node) { +export function toString(node: ParseNode) { if (!node) return ''; - if (!node.parseType) { - throw new Meteor.Error(`Node does not have a parseType defined, node is type ${typeof node} with parseType ${node.parseType}`) - } - let type = nodeTypeIndex[node.parseType]; + const type = nodeTypeIndex[node.parseType]; if (!type?.toString) { throw new Meteor.Error('toString not implemented on ' + node.parseType); } @@ -40,9 +44,9 @@ export function toPrimitiveOrString(node) { return toString(node); } -export function traverse(node, fn) { +export function traverse(node: ParseNode, fn: (ParseNode) => any): ReturnType { if (!node) return; - let type = nodeTypeIndex[node.parseType]; + const type = nodeTypeIndex[node.parseType]; if (!type) { console.error(node); throw new Meteor.Error('Not valid parse node'); @@ -53,9 +57,9 @@ export function traverse(node, fn) { return fn(node); } -export function map(node, fn) { +export function map(node: ParseNode, fn: (ParseNode) => any): ReturnType { if (!node) return; - let type = nodeTypeIndex[node.parseType]; + const type = nodeTypeIndex[node.parseType]; if (!type) { console.error(node); throw new Meteor.Error('Not valid parse node'); @@ -66,13 +70,23 @@ export function map(node, fn) { return fn(node); } +export type ResolvedResult = { + result: ParseNode, + context: Context +} + export class Context { + errors: (Error | { type: string, message: string })[]; + rolls: { number: number, diceSize: number, values: number[] }[]; + options: { [key: string]: any }; + constructor({ errors = [], rolls = [], options = {} } = {}) { this.errors = errors; this.rolls = rolls; this.options = options; } - error(e) { + + error(e: Error | string) { if (!e) return; if (typeof e === 'string') { this.errors.push({ @@ -83,6 +97,7 @@ export class Context { this.errors.push(e); } } + roll(r) { this.rolls.push(r); } diff --git a/app/package.json b/app/package.json index 177a5446..a97a85c4 100644 --- a/app/package.json +++ b/app/package.json @@ -125,7 +125,8 @@ "error", "single" ], - "@typescript-eslint/no-this-alias": "off" + "@typescript-eslint/no-this-alias": "off", + "@typescript-eslint/no-explicit-any": "off" } } } diff --git a/node_modules/@types/nearley/LICENSE b/node_modules/@types/nearley/LICENSE new file mode 100644 index 00000000..9e841e7a --- /dev/null +++ b/node_modules/@types/nearley/LICENSE @@ -0,0 +1,21 @@ + MIT License + + Copyright (c) Microsoft Corporation. + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE diff --git a/node_modules/@types/nearley/README.md b/node_modules/@types/nearley/README.md new file mode 100644 index 00000000..9a5f1a3c --- /dev/null +++ b/node_modules/@types/nearley/README.md @@ -0,0 +1,15 @@ +# Installation +> `npm install --save @types/nearley` + +# Summary +This package contains type definitions for nearley (https://github.com/Hardmath123/nearley#readme). + +# Details +Files were exported from https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nearley. + +### Additional Details + * Last updated: Tue, 07 Nov 2023 09:09:39 GMT + * Dependencies: none + +# Credits +These definitions were written by [Nikita Litvin](https://github.com/deltaidea), and [BendingBender](https://github.com/BendingBender). diff --git a/node_modules/@types/nearley/index.d.ts b/node_modules/@types/nearley/index.d.ts new file mode 100644 index 00000000..5cca1013 --- /dev/null +++ b/node_modules/@types/nearley/index.d.ts @@ -0,0 +1,108 @@ +export as namespace nearley; + +export class Parser { + /** + * Reserved token for indicating a parse fail. + */ + static fail: {}; + + grammar: Grammar; + options: ParserOptions; + lexer: Lexer; + lexerState?: LexerState | undefined; + current: number; + /** + * An array of possible parsings. Each element is the thing returned by your grammar. + * + * Note that this is undefined before the first feed() call. + * It isn't typed as `any[] | undefined` to spare you the null checks when it's definitely an array. + */ + results: any[]; + + constructor(grammar: Grammar, options?: ParserOptions); + + /** + * The Parser object can be fed data in parts with .feed(data). + * You can then find an array of parsings with the .results property. + * If results is empty, then there are no parsings. + * If results contains multiple values, then that combination is ambiguous. + * + * @throws If there are no possible parsings, nearley will throw an error + * whose offset property is the index of the offending token. + */ + feed(chunk: string): this; + finish(): any[]; + restore(column: { [key: string]: any; lexerState: LexerState }): void; + save(): { [key: string]: any; lexerState: LexerState }; +} + +export interface ParserOptions { + keepHistory?: boolean | undefined; + lexer?: Lexer | undefined; +} + +export class Rule { + static highestId: number; + + id: number; + name: string; + symbols: any[]; + postprocess?: Postprocessor | undefined; + + constructor(name: string, symbols: any[], postprocess?: Postprocessor); + + toString(withCursorAt?: number): string; +} + +export class Grammar { + static fromCompiled(rules: CompiledRules): Grammar; + + rules: Rule[]; + start: string; + byName: { [ruleName: string]: Rule[] }; + lexer?: Lexer | undefined; + + constructor(rules: Rule[]); +} + +export interface CompiledRules { + Lexer?: Lexer | undefined; + ParserStart: string; + ParserRules: ParserRule[]; +} + +export interface ParserRule { + name: string; + symbols: any[]; + postprocess?: Postprocessor | undefined; +} + +export type Postprocessor = (data: any[], reference?: number, wantedBy?: {}) => void; + +export interface Lexer { + /** + * Sets the internal buffer to data, and restores line/col/state info taken from save(). + */ + reset(data: string, state?: LexerState): void; + /** + * Returns e.g. {type, value, line, col, …}. Only the value attribute is required. + */ + next(): Token | undefined; + /** + * Returns an object describing the current line/col etc. This allows us + * to preserve this information between feed() calls, and also to support Parser#rewind(). + * The exact structure is lexer-specific; nearley doesn't care what's in it. + */ + save(): LexerState; + /** + * Returns a string with an error message describing the line/col of the offending token. + * You might like to include a preview of the line in question. + */ + formatError(token: Token, message: string): string; +} + +export type Token = string | { value: string }; + +export interface LexerState { + [x: string]: any; +} diff --git a/node_modules/@types/nearley/package.json b/node_modules/@types/nearley/package.json new file mode 100644 index 00000000..72c1d0bd --- /dev/null +++ b/node_modules/@types/nearley/package.json @@ -0,0 +1,58 @@ +{ + "_from": "@types/nearley", + "_id": "@types/nearley@2.11.5", + "_inBundle": false, + "_integrity": "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA==", + "_location": "/@types/nearley", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "@types/nearley", + "name": "@types/nearley", + "escapedName": "@types%2fnearley", + "scope": "@types", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz", + "_shasum": "9087e1634e1c90efb25d661390702381789685cb", + "_spec": "@types/nearley", + "_where": "/Users/stef/github/DiceCloud", + "bugs": { + "url": "https://github.com/DefinitelyTyped/DefinitelyTyped/issues" + }, + "bundleDependencies": false, + "contributors": [ + { + "name": "Nikita Litvin", + "url": "https://github.com/deltaidea" + }, + { + "name": "BendingBender", + "url": "https://github.com/BendingBender" + } + ], + "dependencies": {}, + "deprecated": false, + "description": "TypeScript definitions for nearley", + "homepage": "https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/nearley", + "license": "MIT", + "main": "", + "name": "@types/nearley", + "repository": { + "type": "git", + "url": "git+https://github.com/DefinitelyTyped/DefinitelyTyped.git", + "directory": "types/nearley" + }, + "scripts": {}, + "typeScriptVersion": "4.5", + "types": "index.d.ts", + "typesPublisherContentHash": "2b82830a1a87ef19e588c4f6dcd1c00fde50afd7c9dc5bd8233b054f436578d4", + "version": "2.11.5" +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..4cd093cc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "@types/nearley": { + "version": "2.11.5", + "resolved": "https://registry.npmjs.org/@types/nearley/-/nearley-2.11.5.tgz", + "integrity": "sha512-dM7TrN0bVxGGXTYGx4YhGear8ysLO5SOuouAWM9oltjQ3m9oYa13qi8Z1DJp5zxVMPukvQdsrnZmgzpeuTSEQA==" + } + } +}