From f247b74ddbee8c40eb2134e845274817f9278410 Mon Sep 17 00:00:00 2001 From: Hans Delano <43413899+hanscau@users.noreply.github.com> Date: Sat, 2 Mar 2024 21:54:12 +0800 Subject: [PATCH] 1562 stepper incorrect evaluation sequence in program (#1563) * add: check if second statement is reducible and reduce it * add: test for #1562 * remove: obvious comments * add: second statement checks for BlockStatement and ExpressionStatement * fix: let to const * add: more tests * add: comments for secondStatement array destructuring --- .../__tests__/__snapshots__/stepper.ts.snap | 373 +++++++++++++++++- src/stepper/__tests__/stepper.ts | 113 +++++- src/stepper/stepper.ts | 129 +++++- 3 files changed, 582 insertions(+), 33 deletions(-) diff --git a/src/stepper/__tests__/__snapshots__/stepper.ts.snap b/src/stepper/__tests__/__snapshots__/stepper.ts.snap index e776ce6fe..af8c11c1b 100644 --- a/src/stepper/__tests__/__snapshots__/stepper.ts.snap +++ b/src/stepper/__tests__/__snapshots__/stepper.ts.snap @@ -19,16 +19,20 @@ a(); (() => {})(); \\"Gets returned by normal run\\"; -(() => {})(); +\\"other statement\\"; +{}; \\"Gets returned by normal run\\"; -(() => {})(); +\\"other statement\\"; +{}; \\"Gets returned by normal run\\"; -{}; +\\"other statement\\"; +undefined; \\"Gets returned by normal run\\"; -{}; +\\"other statement\\"; +undefined; \\"Gets returned by normal run\\"; undefined; @@ -62,16 +66,20 @@ a(); a(); \\"Gets returned by normal run\\"; -a(); +\\"other statement\\"; +{}; \\"Gets returned by normal run\\"; -a(); +\\"other statement\\"; +{}; \\"Gets returned by normal run\\"; -{}; +\\"other statement\\"; +undefined; \\"Gets returned by normal run\\"; -{}; +\\"other statement\\"; +undefined; \\"Gets returned by normal run\\"; undefined; @@ -4108,6 +4116,355 @@ f(); " `; +exports[`Test correct evaluation sequence when first statement is a value Irreducible second statement in block 1`] = ` +"{ + 'value'; + 'also a value'; +} + +{ + 'value'; + 'also a value'; +} + +{ + 'also a value'; +} + +{ + 'also a value'; +} + +'also a value'; + +'also a value'; +" +`; + +exports[`Test correct evaluation sequence when first statement is a value Irreducible second statement in functions 1`] = ` +"function f() { + 'value'; + 'also a value'; +} +f(); + +function f() { + 'value'; + 'also a value'; +} +f(); + +f(); + +f(); + +{ + 'value'; + 'also a value'; +}; + +{ + 'value'; + 'also a value'; +}; + +{ + 'also a value'; +}; + +{ + 'also a value'; +}; + +undefined; + +undefined; +" +`; + +exports[`Test correct evaluation sequence when first statement is a value Irreducible second statement in program 1`] = ` +"'value'; +'also a value'; + +'value'; +'also a value'; + +'also a value'; + +'also a value'; +" +`; + +exports[`Test correct evaluation sequence when first statement is a value Mix statements 1`] = ` +"'value'; +const x = 10; +function f() { + 20; + function p() { + 22; + } +} +const z = 30; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +const x = 10; +function f() { + 20; + function p() { + 22; + } +} +const z = 30; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +function f() { + 20; + function p() { + 22; + } +} +const z = 30; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +function f() { + 20; + function p() { + 22; + } +} +const z = 30; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +const z = 30; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +const z = 30; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'value'; +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'also a value'; +{ + 'another value'; + const a = 40; + a; +} +'another value'; +const a = 40; + +'also a value'; +{ + 'another value'; + 40; +} +'another value'; +const a = 40; + +'also a value'; +{ + 'another value'; + 40; +} +'another value'; +const a = 40; + +'also a value'; +{ + 40; +} +'another value'; +const a = 40; + +'also a value'; +{ + 40; +} +'another value'; +const a = 40; + +'also a value'; +40; +'another value'; +const a = 40; + +'also a value'; +40; +'another value'; +const a = 40; + +40; +'another value'; +const a = 40; + +40; +'another value'; +const a = 40; + +'another value'; +const a = 40; + +'another value'; +const a = 40; + +'another value'; + +'another value'; +" +`; + +exports[`Test correct evaluation sequence when first statement is a value Reducible second statement in block 1`] = ` +"{ + 'value'; + const x = 10; +} + +{ + 'value'; + const x = 10; +} + +{ + 'value'; +} + +{ + 'value'; +} + +'value'; + +'value'; +" +`; + +exports[`Test correct evaluation sequence when first statement is a value Reducible second statement in function 1`] = ` +"function f() { + 'value'; + const x = 10; +} +f(); + +function f() { + 'value'; + const x = 10; +} +f(); + +f(); + +f(); + +{ + 'value'; + const x = 10; +}; + +{ + 'value'; + const x = 10; +}; + +{ + 'value'; +}; + +{ + 'value'; +}; + +undefined; + +undefined; +" +`; + +exports[`Test correct evaluation sequence when first statement is a value Reducible second statement in program 1`] = ` +"'value'; +const x = 10; + +'value'; +const x = 10; + +'value'; + +'value'; +" +`; + 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 c3a20c87e..e7bd3f3d8 100644 --- a/src/stepper/__tests__/stepper.ts +++ b/src/stepper/__tests__/stepper.ts @@ -77,6 +77,101 @@ const testEvalSteps = (programStr: string, context?: Context) => { return getEvaluationSteps(program, context, options) } +describe('Test correct evaluation sequence when first statement is a value', () => { + test('Reducible second statement in program', async () => { + const code = ` + 'value'; + const x = 10; + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + expect(getLastStepAsString(steps)).toEqual("'value';") + }) + + test('Irreducible second statement in program', async () => { + const code = ` + 'value'; + 'also a value'; + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + expect(getLastStepAsString(steps)).toEqual("'also a value';") + }) + + test('Reducible second statement in block', async () => { + const code = ` + { + 'value'; + const x = 10; + } + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + expect(getLastStepAsString(steps)).toEqual("'value';") + }) + + test('Irreducible second statement in block', async () => { + const code = ` + { + 'value'; + 'also a value'; + } + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + expect(getLastStepAsString(steps)).toEqual("'also a value';") + }) + + test('Reducible second statement in function', async () => { + const code = ` + function f () { + 'value'; + const x = 10; + } + f(); + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + }) + + test('Irreducible second statement in functions', async () => { + const code = ` + function f () { + 'value'; + 'also a value'; + } + f(); + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + }) + + test('Mix statements', async () => { + const code = ` + 'value'; + const x = 10; + function f() { + 20; + function p() { + 22; + } + } + const z = 30; + 'also a value'; + { + 'another value'; + const a = 40; + a; + } + 'another value'; + const a = 40; + ` + const steps = await testEvalSteps(code) + expect(steps.map(x => codify(x[0])).join('\n')).toMatchSnapshot() + expect(getLastStepAsString(steps)).toEqual("'another value';") + }) +}) + describe('Test single line of code is evaluated', () => { test('Constants Declaration', async () => { const code = ` @@ -185,9 +280,11 @@ test('Test two statement substitution', async () => { 21; 3 * 5; - 3 * 5; + 21; + 15; - 3 * 5; + 21; + 15; 15; @@ -800,10 +897,12 @@ test('triple equals work on function', async () => { g === g; f === g; - g === g; + true; + true; f === g; - g === g; + true; + true; f === g; true; @@ -812,9 +911,11 @@ test('triple equals work on function', async () => { true; f === g; - f === g; + true; + false; - f === g; + true; + false; false; diff --git a/src/stepper/stepper.ts b/src/stepper/stepper.ts index 1443f2efd..0542d58f6 100644 --- a/src/stepper/stepper.ts +++ b/src/stepper/stepper.ts @@ -1795,15 +1795,44 @@ function reduceMain( firstStatement.type === 'ExpressionStatement' && isIrreducible(firstStatement.expression, context) ) { - // let stmt - // if (otherStatements.length > 0) { - paths[0].push('body[0]') - paths.push([]) - const stmt = ast.program(otherStatements as es.Statement[]) - // } else { - // stmt = ast.expressionStatement(firstStatement.expression) - // } - return [stmt, context, paths, explain(node)] + // Intentionally ignore the remaining statements + const [secondStatement] = otherStatements + + if ( + secondStatement !== undefined && + secondStatement.type == 'ExpressionStatement' && + isIrreducible(secondStatement.expression, context) + ) { + paths[0].push('body[0]') + paths.push([]) + const stmt = ast.program(otherStatements as es.Statement[]) + return [stmt, context, paths, explain(node)] + } else { + // Reduce the second statement and preserve the first statement + // Pass in a new path to avoid modifying the original path + const newPath = [[]] + const [reduced, cont, path, str] = reducers['Program']( + ast.program(otherStatements as es.Statement[]), + context, + newPath + ) + + // Fix path highlighting after preserving first statement + path.forEach(pathStep => { + pathStep.forEach((_, i) => { + if (i == 0) { + pathStep[i] = pathStep[i].replace(/\d+/g, match => String(Number(match) + 1)) + } + }) + }) + paths[0].push(...path[0]) + + const stmt = ast.program([ + firstStatement, + ...((reduced as es.Program).body as es.Statement[]) + ]) + return [stmt, cont, path, str] + } } else if (firstStatement.type === 'FunctionDeclaration') { if (firstStatement.id === null) { throw new Error( @@ -1972,15 +2001,46 @@ function reduceMain( firstStatement.type === 'ExpressionStatement' && isIrreducible(firstStatement.expression, context) ) { - let stmt - if (otherStatements.length > 0) { + // Intentionally ignore the remaining statements + const [secondStatement] = otherStatements + + if (secondStatement == undefined) { + const stmt = ast.expressionStatement(firstStatement.expression) + return [stmt, context, paths, explain(node)] + } else if ( + secondStatement.type == 'ExpressionStatement' && + isIrreducible(secondStatement.expression, context) + ) { paths[0].push('body[0]') paths.push([]) - stmt = ast.blockStatement(otherStatements as es.Statement[]) + const stmt = ast.blockStatement(otherStatements as es.Statement[]) + return [stmt, context, paths, explain(node)] } else { - stmt = ast.expressionStatement(firstStatement.expression) + // Reduce the second statement and preserve the first statement + // Pass in a new path to avoid modifying the original path + const newPath = [[]] + const [reduced, cont, path, str] = reducers['BlockStatement']( + ast.blockStatement(otherStatements as es.Statement[]), + context, + newPath + ) + + // Fix path highlighting after preserving first statement + path.forEach(pathStep => { + pathStep.forEach((_, i) => { + if (i == 0) { + pathStep[i] = pathStep[i].replace(/\d+/g, match => String(Number(match) + 1)) + } + }) + }) + paths[0].push(...path[0]) + + const stmt = ast.blockStatement([ + firstStatement, + ...((reduced as es.BlockStatement).body as es.Statement[]) + ]) + return [stmt, cont, paths, str] } - return [stmt, context, paths, explain(node)] } else if (firstStatement.type === 'FunctionDeclaration') { let funDecExp = ast.functionDeclarationExpression( firstStatement.id!, @@ -2148,15 +2208,46 @@ function reduceMain( firstStatement.type === 'ExpressionStatement' && isIrreducible(firstStatement.expression, context) ) { - let stmt - if (otherStatements.length > 0) { + // Intentionally ignore the remaining statements + const [secondStatement] = otherStatements + + if (secondStatement == undefined) { + const stmt = ast.identifier('undefined') + return [stmt, context, paths, explain(node)] + } else if ( + secondStatement.type == 'ExpressionStatement' && + isIrreducible(secondStatement.expression, context) + ) { paths[0].push('body[0]') paths.push([]) - stmt = ast.blockExpression(otherStatements as es.Statement[]) + const stmt = ast.blockExpression(otherStatements as es.Statement[]) + return [stmt, context, paths, explain(node)] } else { - stmt = ast.identifier('undefined') + // Reduce the second statement and preserve the first statement + // Pass in a new path to avoid modifying the original path + const newPath = [[]] + const [reduced, cont, path, str] = reducers['BlockExpression']( + ast.blockExpression(otherStatements as es.Statement[]), + context, + newPath + ) + + // Fix path highlighting after preserving first statement + path.forEach(pathStep => { + pathStep.forEach((_, i) => { + if (i == 0) { + pathStep[i] = pathStep[i].replace(/\d+/g, match => String(Number(match) + 1)) + } + }) + }) + paths[0].push(...path[0]) + + const stmt = ast.blockExpression([ + firstStatement, + ...((reduced as es.BlockStatement).body as es.Statement[]) + ]) + return [stmt, cont, paths, str] } - return [stmt, context, paths, explain(node)] } else if (firstStatement.type === 'FunctionDeclaration') { let funDecExp = ast.functionDeclarationExpression( firstStatement.id!,