From e1637fef24aad6c6b73a66392444db8a770dd42f Mon Sep 17 00:00:00 2001 From: Hans Delano <43413899+hanscau@users.noreply.github.com> Date: Mon, 1 Apr 2024 23:02:49 +0800 Subject: [PATCH] 1622 stepper does not evaluate arguments when it should (#1628) * refactor and rearrange function call reducer * add tests * add more tests * change the order of operation * removed redundant checks * edit tests to new implementation --- .../__tests__/__snapshots__/stepper.ts.snap | 136 ++++++++++++++---- src/stepper/__tests__/stepper.ts | 79 ++++++++-- src/stepper/stepper.ts | 125 +++++++--------- 3 files changed, 232 insertions(+), 108 deletions(-) diff --git a/src/stepper/__tests__/__snapshots__/stepper.ts.snap b/src/stepper/__tests__/__snapshots__/stepper.ts.snap index e6a2a2489..4548c5f07 100644 --- a/src/stepper/__tests__/__snapshots__/stepper.ts.snap +++ b/src/stepper/__tests__/__snapshots__/stepper.ts.snap @@ -4117,58 +4117,123 @@ f(); " `; -exports[`Test catching errors from built in function Incorrect number of arguments 1`] = ` -"Start of evaluation -Line 1: Expected 2 arguments, but got 1" +exports[`Test calling functions Argument reduction steps 1`] = ` +"(1 * 3)(2 * 3 + 10); + +(1 * 3)(2 * 3 + 10); + +(3)(2 * 3 + 10); + +(3)(2 * 3 + 10); + +(3)(6 + 10); + +(3)(6 + 10); + +(3)(16); + +(3)(16); +" `; -exports[`Test catching errors from built in function Incorrect type of argument for math function 1`] = ` -"Start of evaluation -Line 2: Math functions must be called with number arguments" +exports[`Test calling functions Built-in function 1`] = ` +"is_boolean(false); + +is_boolean(false); + +true; + +true; +" `; -exports[`Test catching errors from built in function Incorrect type of arguments for module function 1`] = ` -"Start of evaluation -Line 2: arity expects a function as argument" +exports[`Test calling functions Function that exists 1`] = ` +"function foo(x) { + return x; +} +foo(1 + 2); + +function foo(x) { + return x; +} +foo(1 + 2); + +foo(1 + 2); + +foo(1 + 2); + +foo(3); + +foo(3); + +3; + +3; +" `; -exports[`Test catching of undeclared variable error Variable not declared in block statement 1`] = `"Line 3: Name undeclared_variable not declared."`; +exports[`Test calling functions Imported module function 1`] = ` +"pair(1, 1); -exports[`Test catching of undeclared variable error Variable not declared in function declaration 1`] = `"Line 3: Name undeclared_variable not declared."`; +pair(1, 1); -exports[`Test catching of undeclared variable error Variable not declared in program 1`] = `"Line 2: Name undeclared_variable not declared."`; +[1, 1]; -exports[`Test catching runtime errors Calling non function value 1`] = ` -"Start of evaluation -Binary expression 2 + 3 evaluated -Binary expression 2 + 3 evaluated -Line 2: Calling non-function value 5." +[1, 1]; +" `; -exports[`Test catching runtime errors Incorrect number of argument 1`] = ` +exports[`Test calling functions Incorrect number of argument (less) 1`] = ` "Start of evaluation Function foo declared, parameter(s) a required Function foo declared, parameter(s) a required -Line 5: Expected 1 arguments, but got 0." +Line 5: Expected 0 arguments, but got 1." `; -exports[`Test catching runtime errors Incorrect number of argument 2`] = ` +exports[`Test calling functions Incorrect number of argument (more) 1`] = ` "Start of evaluation Function foo declared, parameter(s) a required Function foo declared, parameter(s) a required -Line 5: Expected 1 arguments, but got 3." +Line 5: Expected 3 arguments, but got 1." `; -exports[`Test catching runtime errors Type error 1`] = ` +exports[`Test calling functions Literal function should error 1`] = ` "Start of evaluation -Line 2: Expected number on right hand side of operation, got string." +Line 2: Calling non-function value 1." `; -exports[`Test catching runtime errors Variable not assigned 1`] = ` +exports[`Test calling functions Math function 1`] = ` +"math_abs(-1); + +math_abs(-1); + +1; + +1; +" +`; + +exports[`Test catching errors from built in function Incorrect number of arguments 1`] = ` "Start of evaluation -Line 2: Name unassigned_variable declared later in current scope but not yet assigned" +" `; +exports[`Test catching errors from built in function Incorrect type of argument for math function 1`] = ` +"Start of evaluation +" +`; + +exports[`Test catching errors from built in function Incorrect type of arguments for module function 1`] = ` +"Start of evaluation +" +`; + +exports[`Test catching of undeclared variable error Variable not declared in block statement 1`] = `"Line 3: Name undeclared_variable not declared."`; + +exports[`Test catching of undeclared variable error Variable not declared in function declaration 1`] = `"Line 3: Name undeclared_variable not declared."`; + +exports[`Test catching of undeclared variable error Variable not declared in program 1`] = `"Line 2: Name undeclared_variable not declared."`; + exports[`Test correct evaluation sequence when first statement is a value Irreducible second statement in block 1`] = ` "{ 'value'; @@ -4655,6 +4720,27 @@ exports[`Test reducing of empty block into epsilon Empty blocks in block 1`] = ` " `; +exports[`Test runtime errors Incompatible types operation 1`] = ` +"Start of evaluation +Binary expression 2 * 3 evaluated +Binary expression 2 * 3 evaluated +Line 2: Expected string on right hand side of operation, got number." +`; + +exports[`Test runtime errors Variable used before assigning in functions 1`] = ` +"Start of evaluation +Function foo declared +Function foo declared +Function foo runs +Function foo runs +Line 3: Name unassigned_variable declared later in current scope but not yet assigned" +`; + +exports[`Test runtime errors Variable used before assigning in program 1`] = ` +"Start of evaluation +Line 2: Name unassigned_variable declared later in current scope but not yet assigned" +`; + exports[`const declarations in blocks subst into call expressions 1`] = ` "const z = 1; function f(g) { diff --git a/src/stepper/__tests__/stepper.ts b/src/stepper/__tests__/stepper.ts index 803308b7a..3f739ba23 100644 --- a/src/stepper/__tests__/stepper.ts +++ b/src/stepper/__tests__/stepper.ts @@ -85,33 +85,57 @@ const testEvalSteps = (programStr: string, context?: Context) => { return getEvaluationSteps(program, context, options) } -describe('Test catching runtime errors', () => { - test('Variable not assigned', async () => { +describe('Test calling functions', () => { + test('Function that exists', async () => { const code = ` - unassigned_variable; - const unassigned_variable = "value"; + function foo(x) { return x;} + foo(1 + 2); ` const steps = await testEvalSteps(code) - expect(getExplanation(steps)).toMatchSnapshot() + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() }) - test('Type error', async () => { + test('Math function', async () => { const code = ` - 1 + "string"; + math_abs(-1); ` const steps = await testEvalSteps(code) - expect(getExplanation(steps)).toMatchSnapshot() + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + }) + + test('Imported module function', async () => { + const code = ` + pair(1, 1); + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + }) + + test('Built-in function', async () => { + const code = ` + is_boolean(false); + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + }) + + test('Argument reduction steps', async () => { + const code = ` + (1 * 3)(2 * 3 + 10); + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() }) - test('Calling non function value', async () => { + test('Literal function should error', async () => { const code = ` - (2 + 3)(1 - 4); + 1(2); ` const steps = await testEvalSteps(code) expect(getExplanation(steps)).toMatchSnapshot() }) - test('Incorrect number of argument', async () => { + test('Incorrect number of argument (less)', async () => { const code = ` function foo(a) { return a; @@ -122,7 +146,7 @@ describe('Test catching runtime errors', () => { expect(getExplanation(steps)).toMatchSnapshot() }) - test('Incorrect number of argument', async () => { + test('Incorrect number of argument (more)', async () => { const code = ` function foo(a) { return a; @@ -134,6 +158,37 @@ describe('Test catching runtime errors', () => { }) }) +describe('Test runtime errors', () => { + test('Variable used before assigning in program', async () => { + const code = ` + unassigned_variable; + const unassigned_variable = "assigned"; + ` + const steps = await testEvalSteps(code) + expect(getExplanation(steps)).toMatchSnapshot() + }) + + test('Variable used before assigning in functions', async () => { + const code = ` + function foo() { + unassigned_variable; + const unassigned_variable = "assigned"; + } + foo(); + ` + const steps = await testEvalSteps(code) + expect(getExplanation(steps)).toMatchSnapshot() + }) + + test('Incompatible types operation', async () => { + const code = ` + "1" + 2 * 3; + ` + const steps = await testEvalSteps(code) + expect(getExplanation(steps)).toMatchSnapshot() + }) +}) + describe('Test catching errors from built in function', () => { test('Incorrect type of argument for math function', async () => { const code = ` diff --git a/src/stepper/stepper.ts b/src/stepper/stepper.ts index 3b0bfcca2..2b0f17370 100644 --- a/src/stepper/stepper.ts +++ b/src/stepper/stepper.ts @@ -1683,6 +1683,8 @@ function reduceMain( const [callee, args] = [node.callee, node.arguments] // source 0: discipline: any expression can be transformed into either literal, ident(builtin) or funexp // if functor can reduce, reduce functor + + //Reduce callee until it is irreducible if (!isIrreducible(callee, context)) { paths[0].push('callee') const [reducedCallee, cont, path, str] = reduce(callee, context, paths) @@ -1692,81 +1694,62 @@ function reduceMain( path, str ] - } else if (callee.type === 'Literal') { + } + + // Reduce all arguments until it is irreducible + for (let i = 0; i < args.length; i++) { + const currentArg = args[i] + if (!isIrreducible(currentArg, context)) { + paths[0].push('arguments[' + i + ']') + const [reducedCurrentArg, cont, path, str] = reduce(currentArg, context, paths) + const reducedArgs = [...args.slice(0, i), reducedCurrentArg, ...args.slice(i + 1)] + return [ + ast.callExpression(callee as es.Expression, reducedArgs as es.Expression[], node.loc), + cont, + path, + str + ] + } + } + + // Error checking for illegal function calls + if (callee.type === 'Literal') { throw new errors.CallingNonFunctionValue(callee.value, node) } else if ( - callee.type === 'Identifier' && - !(callee.name in context.runtime.environments[0].head) + (callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') && + args.length !== callee.params.length ) { - throw new errors.UndefinedVariable(callee.name, callee) + throw new errors.InvalidNumberOfArguments(node, args.length, callee.params.length) + } + + // if it reaches here, means all the arguments are legal. + if (['FunctionExpression', 'ArrowFunctionExpression'].includes(callee.type)) { + // User declared function + return [ + apply(callee as FunctionDeclarationExpression, args as es.Literal[]), + context, + paths, + explain(node) + ] + } else if ((callee as es.Identifier).name.includes('math')) { + // Math function + return [ + builtin.evaluateMath((callee as es.Identifier).name, ...args), + context, + paths, + explain(node) + ] + } else if (typeof builtin[(callee as es.Identifier).name] === 'function') { + // Source specific built-in function + return [builtin[(callee as es.Identifier).name](...args), context, paths, explain(node)] } else { - // callee is builtin or funexp - if ( - (callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') && - args.length !== callee.params.length - ) { - throw new errors.InvalidNumberOfArguments(node, callee.params.length, args.length) - } else { - for (let i = 0; i < args.length; i++) { - const currentArg = args[i] - if (!isIrreducible(currentArg, context)) { - paths[0].push('arguments[' + i + ']') - const [reducedCurrentArg, cont, path, str] = reduce(currentArg, context, paths) - const reducedArgs = [...args.slice(0, i), reducedCurrentArg, ...args.slice(i + 1)] - return [ - ast.callExpression( - callee as es.Expression, - reducedArgs as es.Expression[], - node.loc - ), - cont, - path, - str - ] - } - if ( - currentArg.type === 'Identifier' && - !(currentArg.name in context.runtime.environments[0].head) - ) { - throw new errors.UndefinedVariable(currentArg.name, currentArg) - } - } - } - // if it reaches here, means all the arguments are legal. - if (['FunctionExpression', 'ArrowFunctionExpression'].includes(callee.type)) { - return [ - apply(callee as FunctionDeclarationExpression, args as es.Literal[]), - context, - paths, - explain(node) - ] - } else { - try { - if ((callee as es.Identifier).name.includes('math')) { - return [ - builtin.evaluateMath((callee as es.Identifier).name, ...args), - context, - paths, - explain(node) - ] - } else if (typeof builtin[(callee as es.Identifier).name] === 'function') { - return [ - builtin[(callee as es.Identifier).name](...args), - context, - paths, - explain(node) - ] - } - return [ - builtin.evaluateModuleFunction((callee as es.Identifier).name, context, ...args), - context, - paths, - explain(node) - ] - } catch (error) { - throw new errors.BuiltInFunctionError(callee, error.message) - } - } + // Common built-in function + return [ + builtin.evaluateModuleFunction((callee as es.Identifier).name, context, ...args), + context, + paths, + explain(node) + ] } },