diff --git a/app/imports/parser/functions.js b/app/imports/parser/functions.js index c89322ab..b1ec5a91 100644 --- a/app/imports/parser/functions.js +++ b/app/imports/parser/functions.js @@ -1,3 +1,5 @@ +import ArrayNode from '/imports/parser/parseTree/ArrayNode.js'; + export default { 'abs': { comment: 'Returns the absolute value of a number', @@ -5,7 +7,7 @@ export default { {input: 'abs(9)', result: '9'}, {input: 'abs(-3)', result: '3'}, ], - argumentType: 'number', + arguments: ['number'], resultType: 'number', fn: Math.abs, }, @@ -15,21 +17,21 @@ export default { {input: 'sqrt(16)', result: '4'}, {input: 'sqrt(10)', result: '3.1622776601683795'}, ], - argumentType: 'number', + arguments: ['number'], resultType: 'number', fn: Math.sqrt, }, 'max': { comment: 'Returns the largest of the given numbers', - examples: [{input: 'min(12, 6, 3, 168)', result: '168'}], - argumentType: 'number', + examples: [{input: 'max(12, 6, 3, 168)', result: '168'}], + arguments: anyNumberOf('number'), resultType: 'number', fn: Math.max, }, 'min': { comment: 'Returns the smallest of the given numbers', examples: [{input: 'min(12, 6, 3, 168)', result: '3'}], - argumentType: 'number', + arguments: anyNumberOf('number'), resultType: 'number', fn: Math.min, }, @@ -40,7 +42,7 @@ export default { {input: 'round(5.5)', result: '6'}, {input: 'round(5.05)', result: '5'}, ], - argumentType: 'number', + arguments: ['number'], resultType: 'number', fn: Math.round, }, @@ -52,7 +54,7 @@ export default { {input: 'floor(5)', result: '5'}, {input: 'floor(-5.5)', result: '-6'}, ], - argumentType: 'number', + arguments: ['number'], resultType: 'number', fn: Math.floor, }, @@ -64,7 +66,7 @@ export default { {input: 'ceil(5)', result: '5'}, {input: 'ceil(-5.5)', result: '-5'}, ], - argumentType: 'number', + arguments: ['number'], resultType: 'number', fn: Math.ceil, }, @@ -76,7 +78,7 @@ export default { {input: 'trunc(5)', result: '5'}, {input: 'trunc(-5.5)', result: '-5'}, ], - argumentType: 'number', + arguments:[ 'number'], resultType: 'number', fn: Math.trunc, }, @@ -87,8 +89,32 @@ export default { {input: 'sign(3)', result: '1'}, {input: 'sign(0)', result: '0'}, ], - argumentType: 'number', + arguments: ['number'], resultType: 'number', fn: Math.sign, + }, + 'tableLookup': { + comment: 'Returns the index of the last value in the array that is less than the specified amount', + examples: [ + {input: 'tableLookup([100, 300, 900], 457)', result: '2'}, + {input: 'tableLookup([100, 300, 900], 23)', result: '0'}, + {input: 'tableLookup([100, 300, 900, 1200], 900)', result: '3'}, + {input: 'tableLookup([100, 300], 594)', result: '2'}, + ], + arguments: [ArrayNode, 'number'], + resultType: 'number', + fn: function tableLookup(arrayNode, number){ + for(let i in arrayNode.values){ + let node = arrayNode.values[i]; + if (node.value > number) return i; + } + return arrayNode.values.length; + } } } + +function anyNumberOf(type){ + let argumentArray = [type]; + argumentArray.anyLength = true; + return argumentArray; +} diff --git a/app/imports/parser/parseTree/CallNode.js b/app/imports/parser/parseTree/CallNode.js index 12cab9e4..967dd2d5 100644 --- a/app/imports/parser/parseTree/CallNode.js +++ b/app/imports/parser/parseTree/CallNode.js @@ -11,40 +11,64 @@ export default class CallNode extends ParseNode { } resolve(fn, scope, context){ let func = functions[this.functionName]; + // Check that the function exists if (!func) return new ErrorNode({ node: this, - error: `${this.functionName} is not a function`, + error: `${this.functionName} is not a supported function`, context, }); - let args = castArgsToType({fn, scope, context, args: this.args, type: func.argumentType}); - if (args.failed){ - if (fn === 'reduce'){ + + // Resolve the arguments + let resolvedArgs = this.args.map(node => node[fn](scope, context)); + + // Check that the arguments match what is expected + let checkFailed = this.checkArugments({ + fn, + context, + resolvedArgs, + argumentsExpected: func.arguments + }); + + if (checkFailed){ + if (fn !== 'reduce'){ return new ErrorNode({ node: this, - error: 'Could not convert all arguments to the correct type', - context, + error: `Invalid arguments to ${this.functionName} function`, }); } else { return new CallNode({ functionName: this.functionName, - args: args, + args: resolvedArgs, }); } - } else { - try { - let value = func.fn.apply(null, args); - return new ConstantNode({ - value, - type: 'number', - previousNodes: [this], - }); - } catch (error) { - return new ErrorNode({ - node: this, - error, - context, - }); + } + + // Map contant nodes to constants before attempting to run the function + let mappedArgs = resolvedArgs.map(node => { + if (node instanceof ConstantNode){ + return node.value; + } else { + return node; } + }); + + try { + // Run the function + let value = func.fn.apply(null, mappedArgs); + + let type = typeof value; + if (type === 'number' || type === 'string' || type === 'boolean'){ + // Convert constant results into constant nodes + return new ConstantNode({ value, type }); + } else { + return value; + } + } catch (error) { + return new ErrorNode({ + node: this, + error: error.message || error, + context, + }); } } toString(){ @@ -57,20 +81,47 @@ export default class CallNode extends ParseNode { replaceChildren(fn){ this.args = this.args.map(arg => arg.replaceNodes(fn)); } -} + checkArugments({fn, context, argumentsExpected, resolvedArgs}){ + // Check that the number of arguments matches the number expected + if ( + !argumentsExpected.anyLength && + argumentsExpected.length !== resolvedArgs.length + ){ + context.storeError({ + type: 'error', + message: 'Incorrect number of arguments ' + + `to ${this.functionName} function, ` + + `expected ${argumentsExpected.length} got ${resolvedArgs.length}` + }); + return true; + } -function castArgsToType({fn, scope, context, args, type}){ - let resolvedArgs = args.map(node => node[fn](scope, context)) - let result = []; - if (type === 'number'){ - resolvedArgs.forEach(node => { - if (node.isNumber){ - result.push(node.value); + let failed = false; + // Check that each argument is of the correct type + resolvedArgs.forEach((node, index) => { + let type; + if (argumentsExpected.anyLength){ + type = argumentsExpected[0]; } else { - resolvedArgs.failed = true; + type = argumentsExpected[index]; } - }) + if (typeof type === 'string'){ + // Type being a string means a constant node with matching type + if (node.type !== type) failed = true; + } else { + // Otherwise check that the node is an instance of the given type + if (!(node instanceof type)) failed = true; + } + if (failed && fn === 'reduce'){ + let typeName = typeof type === 'string' ? type : type.constructor.name; + let nodeName = node.type || node.constructor.name + context.storeError({ + type: 'error', + message: `Incorrect arguments to ${this.functionName} function` + + `expected ${typeName} got ${nodeName}` + }); + } + }); + return failed; } - if (resolvedArgs.failed) return resolvedArgs; - return result; }