From b41d26b3add5be252e576147e4a267757828f544 Mon Sep 17 00:00:00 2001 From: Thaum Rystra <9525416+ThaumRystra@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:36:32 +0200 Subject: [PATCH] Fixed failing tests, tested parser more --- .../computeByType/computeCalculation.js | 2 - .../tests/computeAction.testFn.js | 2 +- .../tests/computeCalculations.testFn.js | 1 - app/imports/api/properties/Actions.ts | 1 + app/imports/parser/functions.ts | 6 +- app/imports/parser/parseTree/accessor.test.ts | 139 ++++++++++++++++++ app/imports/parser/parseTree/accessor.ts | 40 +++-- app/imports/parser/parseTree/call.test.ts | 14 ++ app/imports/parser/parseTree/call.ts | 20 +-- app/imports/parser/parseTree/constant.ts | 12 +- app/imports/parser/parseTree/index.ts | 12 -- app/package-lock.json | 6 + app/package.json | 1 + 13 files changed, 211 insertions(+), 45 deletions(-) create mode 100644 app/imports/parser/parseTree/accessor.test.ts create mode 100644 app/imports/parser/parseTree/call.test.ts diff --git a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js index 78c2b86c..46852665 100644 --- a/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js +++ b/app/imports/api/engine/computation/computeComputation/computeByType/computeCalculation.js @@ -40,8 +40,6 @@ export async function resolveCalculationNode(calculation, parseNode, scope, give const fn = calculation._parseLevel; const calculationScope = { ...calculation._localScope, ...scope }; const { result: resultNode, context } = await resolve(fn, parseNode, calculationScope, givenContext); - if (calculation.hash === 1318417319946211 && calculation._key === 'attackRoll') console.log({ calculation, resultNode, parseNode, ers: context.errors }) - calculation.errors = context.errors; calculation.valueNode = resultNode; } diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js index bf4087c1..6bec5740 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeAction.testFn.js @@ -57,7 +57,7 @@ var testProperties = [ }], }, uses: { - calculation: 'nonExistantProperty + 7', + calculation: 'nonExistentProperty + 7', }, usesUsed: 5, left: 1, diff --git a/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js b/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js index bdbe0b65..cc762ae1 100644 --- a/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js +++ b/app/imports/api/engine/computation/computeComputation/tests/computeCalculations.testFn.js @@ -8,7 +8,6 @@ export default async function () { await computeCreatureComputation(computation); const prop = id => computation.propsById[id]; // Tag targeted effects make complicated parse trees - console.log(prop('attackAction2')); assert.equal(prop('attackAction2').attackRoll.value, 'min(3 + d4, d100)', 'Tag targeted effects change the attack roll correctly'); // Tags target effects on attributes assert.equal(prop('taggedCon').value, 26, 'Tagged targeted effects affect attribute values'); diff --git a/app/imports/api/properties/Actions.ts b/app/imports/api/properties/Actions.ts index 068db2a6..f4d87bdc 100644 --- a/app/imports/api/properties/Actions.ts +++ b/app/imports/api/properties/Actions.ts @@ -235,6 +235,7 @@ const ComputedOnlyActionSchema = createPropertySchema({ optional: true, }, uses: { + parseLevel: 'reduce', type: 'computedOnlyField', optional: true, }, diff --git a/app/imports/parser/functions.ts b/app/imports/parser/functions.ts index acb445bf..43461522 100644 --- a/app/imports/parser/functions.ts +++ b/app/imports/parser/functions.ts @@ -132,8 +132,8 @@ const parserFunctions: { [name: string]: ParserFunction } = { ], arguments: ['parseNode'], resultType: 'parseNode', - fn: function resolveFn(node) { - const { result } = resolve('reduce', node, this.scope, this.context); + fn: async function resolveFn(node) { + const { result } = await resolve('reduce', node, this.scope, this.context); return result; } }, @@ -268,7 +268,7 @@ const parserFunctions: { [name: string]: ParserFunction } = { } function anyNumberOf(type) { - const argumentArray = [type]; + const argumentArray: any = [type]; argumentArray.anyLength = true; return argumentArray; } diff --git a/app/imports/parser/parseTree/accessor.test.ts b/app/imports/parser/parseTree/accessor.test.ts new file mode 100644 index 00000000..a6e01bc5 --- /dev/null +++ b/app/imports/parser/parseTree/accessor.test.ts @@ -0,0 +1,139 @@ +import inputProviderForTests from '/imports/api/engine/action/functions/inputProviderForTests.testFn'; +import { parse } from '/imports/parser/parser'; +import resolve from '/imports/parser/resolve'; +import toString from '/imports/parser/toString'; +import { assert } from 'chai'; + +describe('Accessor Node', function () { + it('compiles', async function () { + const callNode = parse('unknownVariable + knownVariable + 1'); + const scope = { + knownVariable: { value: 7 }, + }; + const { result, context } = await resolve( + 'compile', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(context.errors); + assert.equal( + toString(result), + 'unknownVariable + 8', + 'Only known variables should be substituted during compilation step' + ); + }); + it('reduces', async function () { + const callNode = parse('unknownVariable + knownVariable + 1'); + const scope = { + knownVariable: { value: 7 }, + }; + const { result, context } = await resolve( + 'reduce', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(context.errors); + assert.equal( + toString(result), + '8', + 'All variables should be substituted during reduce step' + ); + }); + it('marks undefined variables', async function () { + const callNode = parse('unknownVariable'); + + // At compile step + const { result: compileResult, context: compileContext } = await resolve( + 'compile', callNode, undefined, undefined, inputProviderForTests + ); + assert.isEmpty(compileContext.errors, 'compiling unknown variables should not have errors'); + assert.deepEqual( + compileResult, { parseType: 'accessor', name: 'unknownVariable', isUndefined: true }, + 'Unknown variables should be marked as inUndefined in compile step' + ); + + // At reduce step + const { result: reduceResult, context: reduceContext } = await resolve( + 'reduce', callNode, undefined, undefined, inputProviderForTests + ); + assert.isEmpty(reduceContext.errors, 'reducing unknown variables should not have errors'); + assert.deepEqual( + reduceResult, { parseType: 'constant', value: 0, valueType: 'number', isUndefined: true }, + 'Unknown variables should be marked as inUndefined in compile step' + ); + }); + it('Does not mark isUndefined on known variables', async function () { + const callNode = parse('knownVariable'); + const scope = { + knownVariable: { value: 0 }, + }; + + // At compile step + const { result: compileResult, context: compileContext } = await resolve( + 'compile', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(compileContext.errors, 'compiling known variables should not have errors'); + assert.deepEqual( + compileResult, { parseType: 'constant', value: 0, valueType: 'number' }, + 'Known variables should not be marked as inUndefined in compile step' + ); + + // At reduce step + const { result: reduceResult, context: reduceContext } = await resolve( + 'reduce', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(reduceContext.errors, 'reducing known variables should not have errors'); + assert.deepEqual( + reduceResult, { parseType: 'constant', value: 0, valueType: 'number' }, + 'Known variables should not be marked as inUndefined in compile step' + ); + }); + it('handles .isUndefined on unknown variables', async function () { + const callNode = parse('unknownVariable.isUndefined'); + const scope = { + knownVariable: { value: 7 }, + }; + + // At compile step + const { result: compileResult, context: compileContext } = await resolve( + 'compile', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(compileContext.errors, 'compiling unknown variables should not have errors'); + assert.equal( + toString(compileResult), 'true', + 'Unknown variables should be marked as inUndefined in compile step' + ); + + // At reduce step + const { result: reduceResult, context: reduceContext } = await resolve( + 'reduce', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(reduceContext.errors, 'reducing unknown variables should not have errors'); + assert.equal( + toString(reduceResult), 'true', + 'Unknown variables should be marked as inUndefined in reduce step' + ); + }); + it('handles .isUndefined on known variables', async function () { + const callNode = parse('knownVariable.isUndefined'); + const scope = { + knownVariable: { value: 7 }, + }; + + // At compile step + const { result: compileResult, context: compileContext } = await resolve( + 'compile', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(compileContext.errors, 'compiling unknown variables should not have errors'); + assert.equal( + toString(compileResult), 'false', + 'Unknown variables should not be marked as inUndefined in compile step' + ); + + // At reduce step + const { result: reduceResult, context: reduceContext } = await resolve( + 'reduce', callNode, scope, undefined, inputProviderForTests + ); + assert.isEmpty(reduceContext.errors, 'reducing unknown variables should not have errors'); + assert.equal( + toString(reduceResult), 'false', + 'Known variables should not be marked as inUndefined in reduce step' + ); + }); +}); diff --git a/app/imports/parser/parseTree/accessor.ts b/app/imports/parser/parseTree/accessor.ts index 0d7cb2d3..6b70658c 100644 --- a/app/imports/parser/parseTree/accessor.ts +++ b/app/imports/parser/parseTree/accessor.ts @@ -8,6 +8,7 @@ export type AccessorNode = { parseType: 'accessor' | 'symbol'; path?: string[]; name: string; + isUndefined?: true, } type AccessorFactory = { @@ -18,18 +19,29 @@ type AccessorFactory = { } const accessor: AccessorFactory = { - create({ name, path }: { name: string, path?: string[] }): AccessorNode { + create({ + name, path, isUndefined + }: { + name: string, path?: string[], isUndefined?: true + }): AccessorNode { return { parseType: 'accessor', - path, name, + ...path && { path }, + ...isUndefined && { isUndefined: true }, }; }, async compile(node, scope, context) { let value = getFromScope(node.name, scope); // Get the value from the given path node.path?.forEach(name => { - if (value === undefined) return; + if (name === 'isUndefined') { + value = value === undefined; + return; + } + if (value === undefined) { + return; + } value = value[name]; }); let valueType = getType(value); @@ -75,14 +87,17 @@ const accessor: AccessorFactory = { }; } if (valueType === 'undefined') { - // Undefined defaults to zero + // We are only at compile, if it isn't defined in the scope, return a copy of the accessor return { - result: constant.create({ - value: 0, + result: accessor.create({ + name: node.name, + path: node.path, + isUndefined: true, }), - context + context, }; } + // The type being accessed isn't supported above, make an error and return a copy of the node context.error(`Accessing ${accessor.toString(node)} is not supported yet`); return { result: accessor.create({ @@ -93,18 +108,19 @@ const accessor: AccessorFactory = { }; }, async reduce(node, scope, context, inputProvider, resolveOthers): Promise { - let { result } = await accessor.compile(node, scope, context, inputProvider, resolveOthers); - ({ result } = await resolveOthers('reduce', result, scope, context, inputProvider)); - if (result.parseType === 'accessor') { + // First compile the accessor + const { result } = await accessor.compile(node, scope, context, inputProvider, resolveOthers); + // If compilation didn't find a suitable replacement, return 0 + if (result.parseType === 'accessor' && result.isUndefined) { return { result: constant.create({ value: 0, + isUndefined: true, }), context }; - } else { - return { result, context }; } + return { result, context }; }, toString(node) { if (!node.path?.length) return `${node.name}`; diff --git a/app/imports/parser/parseTree/call.test.ts b/app/imports/parser/parseTree/call.test.ts new file mode 100644 index 00000000..27e8128c --- /dev/null +++ b/app/imports/parser/parseTree/call.test.ts @@ -0,0 +1,14 @@ +import inputProviderForTests from '/imports/api/engine/action/functions/inputProviderForTests.testFn'; +import { parse } from '/imports/parser/parser'; +import resolve from '/imports/parser/resolve'; +import toString from '/imports/parser/toString'; +import { assert } from 'chai'; + +describe('Call Node', function () { + it('compiles', async function () { + const callNode = parse('min( unknownVariable, 1 + 2, 3d30 )'); + const { result, context } = await resolve('compile', callNode, undefined, undefined, inputProviderForTests); + assert.isEmpty(context.errors) + assert.equal(toString(result), 'min(unknownVariable, 3, 3d30)'); + }); +}); diff --git a/app/imports/parser/parseTree/call.ts b/app/imports/parser/parseTree/call.ts index ed9f8020..ee628224 100644 --- a/app/imports/parser/parseTree/call.ts +++ b/app/imports/parser/parseTree/call.ts @@ -108,7 +108,7 @@ const call: CallFactory = { try { // Run the function - const value = func.fn.apply({ + const value = await func.fn.apply({ scope, context, }, mappedArgs); @@ -166,20 +166,22 @@ const call: CallFactory = { let failed = false; // Check that each argument is of the correct type resolvedArgs.forEach((node, index) => { - let type; + let expectedType; if (argumentsExpected.anyLength) { - type = argumentsExpected[0]; + expectedType = argumentsExpected[0]; } else { - type = argumentsExpected[index]; + expectedType = argumentsExpected[index]; } - if (type === 'parseNode') return; + if (expectedType === 'parseNode') return; if ( - node.parseType !== type - && node.parseType === 'constant' - && node.valueType !== type + node.parseType !== expectedType + || ( + node.parseType === 'constant' + && node.valueType !== expectedType + ) ) failed = true; if (failed && fn === 'reduce') { - const typeName = typeof type === 'string' ? type : type.constructor.name; + const typeName = typeof expectedType === 'string' ? expectedType : expectedType.constructor.name; const nodeName = node.parseType; context.error(`Incorrect arguments to ${callNode.functionName} function` + `expected ${typeName} got ${nodeName}`); diff --git a/app/imports/parser/parseTree/constant.ts b/app/imports/parser/parseTree/constant.ts index 43467ffd..390760e7 100644 --- a/app/imports/parser/parseTree/constant.ts +++ b/app/imports/parser/parseTree/constant.ts @@ -1,13 +1,14 @@ import ParseNode from '/imports/parser/parseTree/ParseNode'; import ResolveLevelFunction from '/imports/parser/types/ResolveLevelFunction'; -export type ConstantValueType = number | string | boolean | undefined +export type ConstantValueType = number | string | boolean export type ConstantNode = { parseType: 'constant'; value: ConstantValueType; // TODO replace all `constantNode.valueType` with `typeof constantNode.value` - valueType: 'number' | 'string' | 'boolean' | 'undefined'; + valueType: 'number' | 'string' | 'boolean'; + isUndefined?: true; } export type FiniteNumberConstantNode = { @@ -18,17 +19,18 @@ export type FiniteNumberConstantNode = { } type ConstantFactory = { - create({ value }: { value: ConstantValueType }): ConstantNode; + create({ value, isUndefined }: { value: ConstantValueType, isUndefined?: true }): ConstantNode; compile: ResolveLevelFunction; toString(node: ConstantNode): string; } const constant: ConstantFactory = { - create({ value }): ConstantNode { + create({ value, isUndefined }): ConstantNode { return { parseType: 'constant', - valueType: typeof value as 'number' | 'string' | 'boolean' | 'undefined', + valueType: typeof value as 'number' | 'string' | 'boolean', value, + ...isUndefined && { isUndefined: true } } }, async compile(node, scope, context) { diff --git a/app/imports/parser/parseTree/index.ts b/app/imports/parser/parseTree/index.ts index 75df057a..507d5dda 100644 --- a/app/imports/parser/parseTree/index.ts +++ b/app/imports/parser/parseTree/index.ts @@ -1,5 +1,3 @@ -console.log('index.ts imports') - import accessor from '/imports/parser/parseTree/accessor'; import array from '/imports/parser/parseTree/array'; import call from '/imports/parser/parseTree/call'; @@ -14,8 +12,6 @@ import roll from '/imports/parser/parseTree/roll'; import rollArray from '/imports/parser/parseTree/rollArray'; import unaryOperator from '/imports/parser/parseTree/unaryOperator'; -console.log('index.ts') - const factories = { accessor, array, @@ -34,12 +30,4 @@ const factories = { unaryOperator, }; -console.log('---------------------'); -console.log('---------------------'); -console.log('---------------------'); -console.log(factories.array); -console.log('---------------------'); -console.log('---------------------'); -console.log('---------------------'); - export default factories; diff --git a/app/package-lock.json b/app/package-lock.json index 46533ff3..ee57feb2 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1422,6 +1422,12 @@ "resolved": "https://registry.npmjs.org/@tozd/vue-observer-utils/-/vue-observer-utils-0.5.0.tgz", "integrity": "sha512-HeRxWFJB7FXcQigH2LvauiR0l7hA4qqBC6hK9rBeKf076Ew08C4lx3eo7/YmvADt3b8ZP1j+TN0pGCEhKYOhEA==" }, + "@types/chai": { + "version": "4.3.11", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.11.tgz", + "integrity": "sha512-qQR1dr2rGIHYlJulmr8Ioq3De0Le9E4MJ5AiaeAETJJpndT1uUNHsGFK3L/UIu+rbkQSdj8J/w2bCsBZc/Y5fQ==", + "dev": true + }, "@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", diff --git a/app/package.json b/app/package.json index d51a02ac..f264c12a 100644 --- a/app/package.json +++ b/app/package.json @@ -66,6 +66,7 @@ "vuex": "^3.1.3" }, "devDependencies": { + "@types/chai": "^4.3.11", "@types/mocha": "^10.0.6", "@typescript-eslint/eslint-plugin": "^5.62.0", "@typescript-eslint/parser": "^5.62.0",