diff --git a/src/__tests__/index.ts b/src/__tests__/index.ts index 918ec5d8b..ba4db9f5a 100644 --- a/src/__tests__/index.ts +++ b/src/__tests__/index.ts @@ -104,17 +104,15 @@ test('parseError for template literals with expressions', () => { ) }) -/* Skip the test for now test('Simple arrow function infinite recursion represents CallExpression well', () => { - return expectParsedError('(x => x(x)(x))(x => x(x)(x));').toMatchInlineSnapshot( - `"Line 1: RangeError: Maximum call stack size exceeded"` + return expectParsedError('(x => x(x)(x))(x => x(x)(x));').toContain( + `RangeError: Maximum call stack size exceeded` ) }, 30000) -*/ test('Simple function infinite recursion represents CallExpression well', () => { - return expectParsedError('function f(x) {return x(x)(x);} f(f);').toMatchInlineSnapshot( - `"RangeError: Maximum call stack size exceeded"` + return expectParsedError('function f(x) {return x(x)(x);} f(f);').toContain( + `RangeError: Maximum call stack size exceeded` ) }, 30000) @@ -177,9 +175,7 @@ test('Arrow function infinite recursion with list args represents CallExpression f(list(1, 2)); `, { chapter: Chapter.SOURCE_2 } - ).toMatchInlineSnapshot( - `"Line 2: The function (anonymous) has encountered an infinite loop. It has no base case."` - ) + ).toContain(`RangeError: Maximum call stack size exceeded`) }, 30000) test('Function infinite recursion with list args represents CallExpression well', () => { @@ -195,18 +191,14 @@ test('Arrow function infinite recursion with different args represents CallExpre return expectParsedError(stripIndent` const f = i => f(i+1) - 1; f(0); - `).toMatchInlineSnapshot( - `"Line 2: The function (anonymous) has encountered an infinite loop. It has no base case."` - ) + `).toContain(`RangeError: Maximum call stack size exceeded`) }, 30000) test('Function infinite recursion with different args represents CallExpression well', () => { return expectParsedError(stripIndent` function f(i) { return f(i+1) - 1; } f(0); - `).toMatchInlineSnapshot( - `"Line 2: The function f has encountered an infinite loop. It has no base case."` - ) + `).toContain(`RangeError: Maximum call stack size exceeded`) }, 30000) test('Functions passed into non-source functions remain equal', () => { diff --git a/src/__tests__/tailcall-return.ts b/src/__tests__/tailcall-return.ts index 7f08f80b5..b711e0094 100644 --- a/src/__tests__/tailcall-return.ts +++ b/src/__tests__/tailcall-return.ts @@ -11,7 +11,7 @@ test('Check that stack is at most 10k in size', () => { } } f(10000); - `).toMatchInlineSnapshot(`"Line 5: RangeError: Maximum call stack size exceeded"`) + `).toContain(`Line 5: RangeError: Maximum call stack size exceeded`) }, 10000) test('Simple tail call returns work', () => { diff --git a/src/infiniteLoops/__tests__/errors.ts b/src/infiniteLoops/__tests__/errors.ts deleted file mode 100644 index 52f076aef..000000000 --- a/src/infiniteLoops/__tests__/errors.ts +++ /dev/null @@ -1,81 +0,0 @@ -import * as es from 'estree' - -import { ExceptionError } from '../../errors/errors' -import { RuntimeSourceError } from '../../errors/runtimeSourceError' -import { TimeoutError } from '../../errors/timeoutErrors' -import { mockContext } from '../../utils/testing/mocks' -import { Chapter } from '../../types' -import { - getInfiniteLoopData, - InfiniteLoopError, - InfiniteLoopErrorType, - isPotentialInfiniteLoop, - StackOverflowMessages -} from '../errors' - -const noBaseCaseError = new InfiniteLoopError('f', false, 'test', InfiniteLoopErrorType.NoBaseCase) -const fakePos = { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } } -const emptyProgram: es.Program = { - type: 'Program', - sourceType: 'module', - body: [] -} - -test('timeout errors are potential infinite loops', () => { - const error = new TimeoutError() - expect(isPotentialInfiniteLoop(error)).toBe(true) -}) - -test('stack overflows are potential infinite loops', () => { - const fakePos = { start: { line: 0, column: 0 }, end: { line: 0, column: 0 } } - const makeErrorWithString = (str: string) => new ExceptionError(new Error(str), fakePos) - for (const message of Object.values(StackOverflowMessages)) { - const error = makeErrorWithString(message) - expect(isPotentialInfiniteLoop(error)).toBe(true) - } -}) - -test('other errors are not potential infinite loops', () => { - const runtimeError = new RuntimeSourceError() - const exceptionError = new ExceptionError(new Error('Unexpected'), fakePos) - expect(isPotentialInfiniteLoop(runtimeError)).toBe(false) - expect(isPotentialInfiniteLoop(exceptionError)).toBe(false) -}) - -test('getInfiniteLoopData works when error is directly reported', () => { - const context = mockContext(Chapter.SOURCE_4) - context.errors.push(noBaseCaseError) - - context.previousPrograms.push(emptyProgram) - const result = getInfiniteLoopData(context) - expect(result).toBeDefined() - expect(result?.[0]).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.[3].length).toBe(1) - expect(result?.[3]).toContain(emptyProgram) -}) - -test('getInfiniteLoopData works when error hidden in timeout', () => { - const error: any = new TimeoutError() - error.infiniteLoopError = noBaseCaseError - const context = mockContext(Chapter.SOURCE_4) - context.errors.push(error) - context.previousPrograms.push(emptyProgram) - const result = getInfiniteLoopData(context) - expect(result).toBeDefined() - expect(result?.[0]).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.[3].length).toBe(1) - expect(result?.[3]).toContain(emptyProgram) -}) - -test('getInfiniteLoopData works when error hidden in exceptionError', () => { - const innerError: any = new Error() - innerError.infiniteLoopError = noBaseCaseError - const context = mockContext(Chapter.SOURCE_4) - context.errors.push(new ExceptionError(innerError, fakePos)) - context.previousPrograms.push(emptyProgram) - const result = getInfiniteLoopData(context) - expect(result).toBeDefined() - expect(result?.[0]).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.[3].length).toBe(1) - expect(result?.[3]).toContain(emptyProgram) -}) diff --git a/src/infiniteLoops/__tests__/instrument.ts b/src/infiniteLoops/__tests__/instrument.ts deleted file mode 100644 index e292ef2a0..000000000 --- a/src/infiniteLoops/__tests__/instrument.ts +++ /dev/null @@ -1,170 +0,0 @@ -import { Program } from 'estree' - -import { mockContext } from '../../utils/testing/mocks' -import { parse } from '../../parser/parser' -import { Chapter } from '../../types' -import { evaluateBinaryExpression, evaluateUnaryExpression } from '../../utils/operators' -import { - InfiniteLoopRuntimeFunctions as functionNames, - InfiniteLoopRuntimeObjectNames, - instrument -} from '../instrument' - -function mockFunctionsAndState() { - const theState = undefined - const returnFirst = (...args: any[]) => args[0] - const nothing = (..._args: any[]) => {} - - const functions = { - [functionNames.nothingFunction]: nothing, - [functionNames.concretize]: returnFirst, - [functionNames.hybridize]: returnFirst, - [functionNames.wrapArg]: returnFirst, - [functionNames.dummify]: returnFirst, - [functionNames.saveBool]: returnFirst, - [functionNames.saveVar]: returnFirst, - [functionNames.preFunction]: nothing, - [functionNames.returnFunction]: returnFirst, - [functionNames.postLoop]: (_: any, res?: any) => res, - [functionNames.enterLoop]: nothing, - [functionNames.exitLoop]: nothing, - [functionNames.trackLoc]: (_1: any, _2: any, fn?: any) => (fn ? fn() : undefined), - [functionNames.evalB]: evaluateBinaryExpression, - [functionNames.evalU]: evaluateUnaryExpression - } - return [functions, theState] -} - -/** - * Returns the value saved in the code using the builtin 'output'. - * e.g. runWithMock('output(2)') --> 2 - */ -async function runWithMock( - main: string, - codeHistory?: string[], - builtins: Map = new Map() -) { - let output = undefined - builtins.set('output', (x: any) => (output = x)) - builtins.set('undefined', undefined) - const context = mockContext(Chapter.SOURCE_4) - const program = parse(main, context) - expect(program).not.toBeUndefined() - let previous: Program[] = [] - if (codeHistory !== undefined) { - const restOfCode = codeHistory.map(x => parse(x, context)) - for (const code of restOfCode) { - expect(code).not.toBeUndefined() - } - previous = restOfCode as Program[] - } - const [mockFunctions, mockState] = mockFunctionsAndState() - const instrumentedCode = await instrument(previous, program as Program, builtins.keys()) - const { builtinsId, functionsId, stateId } = InfiniteLoopRuntimeObjectNames - const sandboxedRun = new Function('code', functionsId, stateId, builtinsId, `return eval(code)`) - await sandboxedRun(instrumentedCode, mockFunctions, mockState, builtins) - return output -} - -test('builtins work', () => { - const main = 'output(2);' - return expect(runWithMock(main, [])).resolves.toBe(2) -}) - -test('binary and unary expressions work', () => { - return Promise.all([ - expect(runWithMock('output(1+1);', [])).resolves.toBe(2), - expect(runWithMock('output(!true);', [])).resolves.toBe(false) - ]) -}) - -test('assignment works as expected', () => { - const main = `let x = 2; - let a = []; - a[0] = 3; - output(x+a[0]);` - return expect(runWithMock(main)).resolves.toBe(5) -}) - -test('globals from old code accessible', () => { - const main = 'output(z+1);' - const prev = ['const z = w+1;', 'let w = 10;'] - return expect(runWithMock(main, prev)).resolves.toBe(12) -}) - -test('functions run as expected', () => { - const main = `function f(x,y) { - return x===0?x:f(x-1,y)+y; - } - output(f(5,2));` - return expect(runWithMock(main)).resolves.toBe(10) -}) - -test('nested functions run as expected', () => { - const main = `function f(x,y) { - function f(x,y) { - return 0; - } - return x===0?x:f(x-1,y)+y; - } - output(f(5,2));` - return expect(runWithMock(main)).resolves.toBe(2) -}) - -test('higher order functions run as expected', () => { - const main = `function run(f, x) { - return f(x+1); - } - output(run(x=>x+1, 1));` - return expect(runWithMock(main)).resolves.toBe(3) -}) - -test('loops run as expected', () => { - const main = `let w = 0; - for (let i = w; i < 10; i=i+1) {w = i;} - output(w);` - return expect(runWithMock(main)).resolves.toBe(9) -}) - -test('nested loops run as expected', () => { - const main = `let w = 0; - for (let i = w; i < 10; i=i+1) { - for (let j = 0; j < 10; j=j+1) { - w = w + 1; - } - } - output(w);` - return expect(runWithMock(main)).resolves.toBe(100) -}) - -test('multidimentional arrays work', () => { - const main = `const x = [[1],[2]]; - output(x[1] === undefined? undefined: x[1][0]);` - return expect(runWithMock(main)).resolves.toBe(2) -}) - -test('if statements work as expected', () => { - const main = `let x = 1; - if (x===1) { - x = x + 1; - } else {} - output(x);` - return expect(runWithMock(main)).resolves.toBe(2) -}) - -test('combination of loops and functions run as expected', () => { - const main = `function test(x) { - return x===0; - } - const minus = (a,b) => a-b; - let w = 10; - let z = 0; - while(!test(w)) { - for (let j = 0; j < 10; j=j+1) { - z = z + 1; - } - w = minus(w,1); - } - output(z);` - return expect(runWithMock(main)).resolves.toBe(100) -}) diff --git a/src/infiniteLoops/__tests__/runtime.ts b/src/infiniteLoops/__tests__/runtime.ts deleted file mode 100644 index e12b54793..000000000 --- a/src/infiniteLoops/__tests__/runtime.ts +++ /dev/null @@ -1,295 +0,0 @@ -import type es from 'estree' - -import { runInContext } from '../..' -import createContext from '../../createContext' -import { mockContext } from '../../utils/testing/mocks' -import { parse } from '../../parser/parser' -import { Chapter, Variant } from '../../types' -import { getInfiniteLoopData, InfiniteLoopError, InfiniteLoopErrorType } from '../errors' -import { testForInfiniteLoop } from '../runtime' - -test('works in runInContext when throwInfiniteLoops is true', async () => { - const code = `function fib(x) { - return fib(x-1) + fib(x-2); - } - fib(100000);` - const context = mockContext(Chapter.SOURCE_4) - await runInContext(code, context, { throwInfiniteLoops: true }) - const lastError = context.errors[context.errors.length - 1] - expect(lastError).toBeInstanceOf(InfiniteLoopError) - const result: InfiniteLoopError = lastError as InfiniteLoopError - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.streamMode).toBe(false) -}) - -test('works in runInContext when throwInfiniteLoops is false', async () => { - const code = `function fib(x) { - return fib(x-1) + fib(x-2); - } - fib(100000);` - const context = mockContext(Chapter.SOURCE_4) - await runInContext(code, context, { throwInfiniteLoops: false }) - const lastError: any = context.errors[context.errors.length - 1] - expect(lastError instanceof InfiniteLoopError).toBe(false) - const result = getInfiniteLoopData(context) - expect(result).toBeDefined() - expect(result?.[0]).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.[1]).toBe(false) -}) - -const testForInfiniteLoopWithCode = (code: string, previousPrograms: es.Program[]) => { - const context = createContext(Chapter.SOURCE_4, Variant.DEFAULT) - const program = parse(code, context) - if (program === null) { - throw new Error('Unable to parse code.') - } - - function repeat(func: (arg: T) => T, n: number): (arg: T) => T { - return n === 0 - ? function (x) { - return x - } - : function (x) { - return func(repeat(func, n - 1)(x)) - } - } - function twice(func: (arg: T) => T) { - return repeat(func, 2) - } - function thrice(func: (arg: T) => T) { - return repeat(func, 3) - } - - return testForInfiniteLoop(program, previousPrograms, { - repeat: { repeat, twice, thrice } - }) -} - -test('non-infinite recursion not detected', async () => { - const code = `function fib(x) { - return x<=1?x:fib(x-1) + fib(x-2); - } - fib(100000); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result).toBeUndefined() -}) - -test('non-infinite loop not detected', async () => { - const code = `for(let i = 0;i<2000;i=i+1){i+1;} - let j = 0; - while(j<2000) {j=j+1;} - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result).toBeUndefined() -}) - -test('no base case function detected', async () => { - const code = `function fib(x) { - return fib(x-1) + fib(x-2); - } - fib(100000); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.streamMode).toBe(false) -}) - -test('no base case loop detected', async () => { - const code = `for(let i = 0;true;i=i+1){i+1;} - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.streamMode).toBe(false) -}) - -test('no variables changing function detected', async () => { - const code = `let x = 1; - function f() { - return x===0?x:f(); - } - f(); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) - expect(result?.streamMode).toBe(false) - expect(result?.explain()).toContain('None of the variables are being updated.') -}) - -test('no state change function detected', async () => { - const code = `let x = 1; - function f() { - return x===0?x:f(); - } - f(); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) - expect(result?.streamMode).toBe(false) - expect(result?.explain()).toContain('None of the variables are being updated.') -}) - -test('infinite cycle detected', async () => { - const code = `function f(x) { - return x[0] === 1? x : f(x); - } - f([2,3,4]); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) - expect(result?.streamMode).toBe(false) - expect(result?.explain()).toContain('cycle') - expect(result?.explain()).toContain('[2,3,4]') -}) - -test('infinite data structures detected', async () => { - const code = `function f(x) { - return is_null(x)? x : f(tail(x)); - } - let circ = list(1,2,3); - set_tail(tail(tail(circ)), circ); - f(circ); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) - expect(result?.streamMode).toBe(false) - expect(result?.explain()).toContain('cycle') - expect(result?.explain()).toContain('(CIRCULAR)') -}) - -test('functions using SMT work', async () => { - const code = `function f(x) { - return x===0? x: f(x+1); - } - f(1); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.FromSmt) - expect(result?.streamMode).toBe(false) -}) - -test('detect forcing infinite streams', async () => { - const code = `stream_to_list(integers_from(0));` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.NoBaseCase) - expect(result?.streamMode).toBe(true) -}) - -test('detect mutual recursion', async () => { - const code = `function e(x){ - return x===0?1:1-o(x-1); - } - function o(x){ - return x===1?0:1-e(x-1); - } - e(9);` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.FromSmt) - expect(result?.streamMode).toBe(false) -}) - -test('functions passed as arguments not checked', async () => { - // if they are checked -> this will throw no base case - const code = `const twice = f => x => f(f(x)); - const thrice = f => x => f(f(f(x))); - const add = x => x + 1; - - (thrice)(twice(twice))(twice(add))(0);` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result).toBeUndefined() -}) - -test('detect complicated cycle example', async () => { - const code = `function permutations(s) { - return is_null(s) - ? list(null) - : accumulate(append, null, - map(x => map(p => pair(x, p), - permutations(remove(x, s))), - s)); - } - - function remove_duplicate(xs) { - return is_null(xs) - ? xs - : pair(head(xs), - remove_duplicate(filter(x => x !== equal(head(xs),x), xs))); - } - - remove_duplicate(list(list(1,2,3), list(1,2,3))); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) - expect(result?.streamMode).toBe(false) -}) - -test('detect complicated cycle example 2', async () => { - const code = `function make_big_int_from_number(num){ - let output = num; - while(output !== 0){ - const digits = num % 10; - output = math_floor(num / 10); - - } -} -make_big_int_from_number(1234); - ` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) - expect(result?.streamMode).toBe(false) -}) - -test('detect complicated fromSMT example 2', async () => { - const code = `function fast_power(b,n){ - if (n % 2 === 0){ - return b* fast_power(b, n-2); - } else { - return b * fast_power(b, n-2); - } - - } - fast_power(2,3);` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.FromSmt) - expect(result?.streamMode).toBe(false) -}) - -test('detect complicated stream example', async () => { - const code = `function up(a, b) { - return (a > b) - ? up(1, 1 + b) - : pair(a, () => stream_reverse(up(a + 1, b))); - } - eval_stream(up(1,1), 22);` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result).toBeDefined() - expect(result?.streamMode).toBe(true) -}) - -test('math functions are disabled in smt solver', async () => { - const code = ` - function f(x) { - return x===0? x: f(math_floor(x+1)); - } - f(1);` - const result = await testForInfiniteLoopWithCode(code, []) - expect(result).toBeUndefined() -}) - -test('cycle detection ignores non deterministic functions', () => { - const code = ` - function f(x) { - return x===0?0:f(math_floor(math_random()/2) + 1); - } - f(1);` - const result = testForInfiniteLoopWithCode(code, []) - expect(result).toBeUndefined() -}) - -test('handle imports properly', () => { - const code = `import {thrice} from "repeat"; - function f(x) { return is_number(x) ? f(x) : 42; } - display(f(thrice(x=>x+1)(0)));` - const result = testForInfiniteLoopWithCode(code, []) - expect(result?.infiniteLoopType).toBe(InfiniteLoopErrorType.Cycle) -}) diff --git a/src/infiniteLoops/__tests__/symbolic.ts b/src/infiniteLoops/__tests__/symbolic.ts deleted file mode 100644 index e707ae810..000000000 --- a/src/infiniteLoops/__tests__/symbolic.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as sym from '../symbolic' - -test('make dummy only dummifies concrete values', () => { - const concreteVal = 2 - const hybrid = sym.makeDummyHybrid(concreteVal) - expect(sym.isHybrid(hybrid)).toBe(true) - - const concreteArray = [2] - const notHybrid = sym.makeDummyHybrid(concreteArray) - expect(sym.isHybrid(notHybrid)).toBe(false) - - expect(sym.makeDummyHybrid(hybrid)).toBe(hybrid) -}) - -test('hybridization and concretization are idempotent', () => { - const concreteVal = 2 - const hybrid1 = sym.hybridizeNamed('c', concreteVal) - const hybrid2 = sym.hybridizeNamed('d', hybrid1) - expect(hybrid1).toBe(hybrid2) - const conc = sym.shallowConcretize(hybrid1) - expect(conc).toBe(sym.shallowConcretize(conc)) - - const concreteArray = [2] - const hybridA1 = sym.hybridizeNamed('a', concreteArray) - const hybridA2 = sym.hybridizeNamed('b', hybridA1) - expect(hybridA1).toBe(hybridA2) - const concA = sym.shallowConcretize(hybridA1) - expect(concA).toBe(sym.shallowConcretize(concA)) -}) diff --git a/src/infiniteLoops/detect.ts b/src/infiniteLoops/detect.ts deleted file mode 100644 index 680e0034d..000000000 --- a/src/infiniteLoops/detect.ts +++ /dev/null @@ -1,403 +0,0 @@ -import { generate } from 'astring' -import * as es from 'estree' - -import { simple } from '../utils/walkers' -import { InfiniteLoopError, InfiniteLoopErrorType } from './errors' -import { getOriginalName } from './instrument' -import * as st from './state' -import { shallowConcretize } from './symbolic' - -// eslint-disable-next-line @typescript-eslint/no-require-imports -const runAltErgo: any = require('@joeychenofficial/alt-ergo-modified') - -const options = { - answers_with_loc: false, - input_format: 'Native', - interpretation: 1, - unsat_core: true, - verbose: false, - sat_solver: 'Tableaux', - file: 'smt-file' -} - -/** - * Checks if the program is stuck in an infinite loop. - * @throws InfiniteLoopError if so. - * @returns void otherwise. - */ -export function checkForInfiniteLoop( - stackPositions: number[], - state: st.State, - functionName: string | undefined -) { - const report = (message: string, type: InfiniteLoopErrorType) => { - throw new InfiniteLoopError(functionName, state.streamMode, message, type) - } - if (hasNoBaseCase(stackPositions, state)) { - report('It has no base case.', InfiniteLoopErrorType.NoBaseCase) - } - // arbitrarily using same threshold - - let circular - try { - circular = checkForCycle(stackPositions.slice(stackPositions.length - state.threshold), state) - } catch (e) { - circular = undefined - } - if (circular) { - let message - if (circular[0] === circular[1] && circular[0] === '') { - message = 'None of the variables are being updated.' - } else { - message = 'It has the infinite cycle: ' + circular.join(' -> ') + '.' - } - report(message, InfiniteLoopErrorType.Cycle) - } else { - const code = codeToDispatch(stackPositions, state) - const pass = runUntilValid(code) - if (pass) { - const message = 'In particular, ' + pass - report(message, InfiniteLoopErrorType.FromSmt) - } - } -} - -/** - * If no if statement/conditional was encountered between iterations, there is no base case. - */ -function hasNoBaseCase(stackPositions: number[], state: st.State) { - const thePaths = state.mixedStack.slice(stackPositions[0], stackPositions[1]).map(x => x.paths) - return flatten(thePaths).length === 0 -} - -/** - * @returns if a cycle was detected, string array describing the cycle. Otherwise returns undefined. - */ -function checkForCycle(stackPositions: number[], state: st.State): string[] | undefined { - const hasInvalidTransition = stackPositions.some(x => - st.State.isNonDetTransition(state.mixedStack[x].transitions) - ) - if (hasInvalidTransition) { - return undefined - } - const transitions = stackPositions.map(i => state.mixedStack[i].transitions) - const concStr = [] - for (const item of transitions) { - const innerStr = [] - for (const transition of item) { - if (typeof transition.value === 'function') { - return - } - innerStr.push(`(${getOriginalName(transition.name)}: ${stringifyCircular(transition.value)})`) - } - concStr.push(innerStr.join(', ')) - } - return getCycle(concStr) -} - -function getCycle(temp: any[]) { - const last = temp[temp.length - 1] - const ix1 = temp.lastIndexOf(last, -2) - if (ix1 === -1) return undefined - const period = temp.length - ix1 - 1 - const s1 = temp.slice(ix1 - period, ix1) - const s2 = temp.slice(ix1, -1) - if (s1.length != period) return undefined - for (let i = 0; i < period; i++) { - if (s1[i] != s2[i]) return undefined - } - return s1.concat(s1[0]) -} - -function stringifyCircular(x: any) { - // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#examples - const getCircularReplacer = () => { - const seen = new WeakSet() - return (key: string, value: any) => { - if (typeof value === 'object' && value !== null) { - if (seen.has(value)) { - return '(CIRCULAR)' - } - seen.add(value) - } - return shallowConcretize(value) - } - } - return JSON.stringify(x, getCircularReplacer()) -} - -function runUntilValid(items: [string, () => string][]) { - for (const [code, message] of items) { - const out = runSMT(code) - if (out.includes('Valid')) return message() - } - return undefined -} - -function runSMT(code: string): string { - try { - const input = { content: [code] } - const out = JSON.parse(runAltErgo(input, options)) - return out.results[0] - } catch (e) { - return e.toString() - } -} - -type IterationFrame = { - name: string - prevPaths: number[] - nextPaths: number[] - transition: st.Transition[] -} - -function flatten(arr: T[][]): T[] { - return [].concat(...(arr as any[])) -} - -function arrEquals(a1: T[], a2: T[], cmp = (x: T, y: T) => x === y) { - if (a1.length !== a2.length) return false - for (let i = 0; i < a1.length; i++) { - if (!cmp(a1[i], a2[i])) return false - } - return true -} - -function iterationFrameEquals(t1: IterationFrame, t2: IterationFrame) { - return ( - arrEquals(t1.prevPaths, t2.prevPaths) && - arrEquals(t1.nextPaths, t2.nextPaths) && - arrEquals( - t1.transition, - t2.transition, - (x, y) => x.name === y.name && x.cachedSymbolicValue === y.cachedSymbolicValue - ) && - t1.name === t2.name - ) -} - -function codeToDispatch(stackPositions: number[], state: st.State) { - const firstSeen = getFirstSeen(stackPositions, state) - const closedCycles = getClosed(firstSeen) - const toCheckNested = closedCycles.map(([from, to]) => - toSmtSyntax(firstSeen.slice(from, to + 1), state) - ) - return flatten(toCheckNested) -} - -/** - * Get iteration frames from the stackPositions, ignoring duplicates. - * Preserves order in which the iterations frames are first seen in stackPositions. - */ -function getFirstSeen(stackPositions: number[], state: st.State) { - let firstSeen: IterationFrame[] = [] - for (let i = 1; i < stackPositions.length - 1; i++) { - const prev = stackPositions[i - 1] - const current = stackPositions[i] - const next = stackPositions[i + 1] - const prevPaths = state.mixedStack.slice(prev, current).map(x => x.paths) - const nextPaths = state.mixedStack.slice(current, next).map(x => x.paths) - const transitions = state.mixedStack.slice(prev, current).map(x => x.transitions) - const hasInvalidPath = prevPaths.concat(nextPaths).some(st.State.isInvalidPath) - const hasInvalidTransition = transitions.some(st.State.isInvalidTransition) - if (hasInvalidPath || hasInvalidTransition) { - // if any path or transition is invalid - firstSeen = [] - continue - } - const frame: IterationFrame = { - name: state.mixedStack[current].loc, - prevPaths: flatten(prevPaths), - nextPaths: flatten(nextPaths), - transition: flatten(transitions) - } - let wasSeen = false - for (const seen of firstSeen) { - if (iterationFrameEquals(frame, seen)) { - wasSeen = true - break - } - } - if (!wasSeen) { - firstSeen.push(frame) - } - } - return firstSeen -} - -/** - * Get closed sets of Iteration Frames where each iteration will - * transition into another in the set. - */ -function getClosed(firstSeen: IterationFrame[]) { - const indices: [number, number][] = [] - for (let i = 0; i < firstSeen.length; i++) { - for (let j = 0; j <= i; j++) { - if (arrEquals(firstSeen[i].nextPaths, firstSeen[j].prevPaths)) { - // closed - indices.push([j, i]) - } - } - } - return indices -} - -function joiner(content: string[][]) { - const inner = (x: string[]) => `(${x.join(' and ')})` - return content.map(inner).join(' or ') -} - -function getIds(nodes: es.Expression[][]) { - const result: es.Identifier[] = [] - for (const node of flatten(nodes)) { - simple(node, { - Identifier(node: es.Identifier) { - result.push(node) - } - }) - } - return [...new Set(result)] -} - -function formatTransitionForMessage(transition: st.Transition, state: st.State) { - // this will be run after ids are reverted to their original names - const symbolic = state.idToStringCache[transition.cachedSymbolicValue] - if (symbolic === 'undefined') { - // set as a constant - return `${getOriginalName(transition.name)}' = ${transition.value}` - } else { - const originalExpr = generate(state.idToExprCache[transition.cachedSymbolicValue]) - return `${getOriginalName(transition.name)}' = ${originalExpr}` - } -} - -/** - * Creates a default error message using the pathExprs and transitions. - * May destructively modify the transitions. - */ -function errorMessageMaker( - ids: es.Identifier[], - pathExprs: es.Expression[][], - transitions: st.Transition[][], - state: st.State -) { - return () => { - const idsOfTransitions = getIds( - transitions.map(x => x.map(t => state.idToExprCache[t.cachedSymbolicValue])) - ) - ids = ids.concat(idsOfTransitions) - ids.map(x => (x.name = getOriginalName(x.name))) - const pathPart: string[][] = pathExprs.map(x => x.map(generate)) - const transitionPart = transitions.map(x => x.map(t => formatTransitionForMessage(t, state))) - let result = '' - for (let i = 0; i < transitionPart.length; i++) { - if (i > 0) result += ' And in a subsequent iteration, ' - result += `when (${pathPart[i].join(' and ')}), ` - result += `the variables are updated (${transitionPart[i].join(', ')}).` - } - return result - } -} - -function smtTemplate(mode: string, decls: string, line1: string, line2: string, line3: string) { - const str = `goal g_1: - forall ${decls}:${mode}. - ${line1} -> - ${line2} -> - ${line3}` - return str.replace(/===/g, '=') -} - -function formatTransition(transition: st.Transition, state: st.State) { - const symbolic = state.idToStringCache[transition.cachedSymbolicValue] - if (symbolic === 'undefined') { - // set as a constant - return `${transition.name}' = ${transition.value}` - } else { - return `${transition.name}' = ${symbolic}` - } -} - -/** - * Substitutes path and transition expressions into a template to be executed - * by the SMT solver. - * @returns list of templated code. - */ -function toSmtSyntax(toInclude: IterationFrame[], state: st.State): [string, () => string][] { - const pathStr = toInclude.map(x => x.prevPaths.map(i => state.idToStringCache[i])) - const line1 = joiner(pathStr) - const pathExprs = toInclude.map(x => x.prevPaths.map(i => state.idToExprCache[i])) - const ids = getIds(pathExprs) - // primify - ids.map(x => (x.name = x.name + "'")) - const line3 = joiner(pathExprs.map(x => x.map(generate))) - // unprimify - ids.map(x => (x.name = x.name.slice(0, -1))) - - const transitions = toInclude.map(x => x.transition.filter(t => typeof t.value === 'number')) - const line2 = joiner(transitions.map(x => x.map(t => formatTransition(t, state)))) - const allNames = flatten(transitions.map(x => x.map(y => y.name))).concat(ids.map(x => x.name)) - const decls = [...new Set(allNames)].map(x => `${x},${x}'`).join(',') - const [newLine1, newLine3] = addConstantsAndSigns(line1, line3, transitions, state) - const message = errorMessageMaker(ids, pathExprs, transitions, state) - const template1: [string, () => string] = [ - smtTemplate('int', decls, line1, line2, line3), - message - ] - const template2: [string, () => string] = [ - smtTemplate('int', decls, newLine1, line2, newLine3), - message - ] - return [template1, template2] -} - -/** - * Using information from transitions, add information on constants - * and signs of variables into a new template for lines 1 and 3. - * @returns line 1 and line 3 - */ -function addConstantsAndSigns( - line1: string, - line3: string, - transitions: st.Transition[][], - state: st.State -) { - type TransitionVariable = { - isConstant: boolean - value: number - } - const values = new Map() - for (const transition of flatten(transitions)) { - let item = values.get(transition.name) - const symbolicValue = state.idToStringCache[transition.cachedSymbolicValue] - if (item === undefined) { - item = [] - values.set(transition.name, item) - } - // if var is constant, then transition will be (name)=(name), e.g. "c=c" - item.push({ isConstant: transition.name === symbolicValue, value: transition.value }) - } - const consts = [] - const signs1 = [] - const signs3 = [] - for (const [name, item] of values.entries()) { - if (item.every(x => x.isConstant)) { - consts.push(`${name} = ${item[0].value}`) - } else if (item.every(x => x.value > 0)) { - signs1.push(`${name} > 0`) - signs3.push(`${name}' > 0`) - } else if (item.every(x => x.value < 0)) { - signs1.push(`${name} < 0`) - signs3.push(`${name}' > 0`) - } - } - const innerJoiner = (x: string[]) => `(${x.join(' and ')})` - let newLine1 = line1 - let newLine3 = line3 - if (signs1.length > 0) { - newLine1 = `${line1} and ${innerJoiner(signs1)}` - newLine3 = `${line3} and ${innerJoiner(signs3)}` - } - if (consts.length > 0) newLine1 = `${innerJoiner(consts)} -> ${newLine1}` - return [newLine1, newLine3] -} diff --git a/src/infiniteLoops/errors.ts b/src/infiniteLoops/errors.ts deleted file mode 100644 index 3c86be2d7..000000000 --- a/src/infiniteLoops/errors.ts +++ /dev/null @@ -1,103 +0,0 @@ -import * as es from 'estree' - -import { Context } from '..' -import { ExceptionError } from '../errors/errors' -import { RuntimeSourceError } from '../errors/runtimeSourceError' -import { TimeoutError } from '../errors/timeoutErrors' -import { getOriginalName } from './instrument' - -export enum StackOverflowMessages { - firefox = 'InternalError: too much recursion', - // webkit: chrome + safari. Also works for node - webkit = 'RangeError: Maximum call stack size exceeded', - edge = 'Error: Out of stack space' -} - -/** - * Checks if the error is a TimeoutError or Stack Overflow. - * - * @returns {true} if the error is a TimeoutError or Stack Overflow. - * @returns {false} otherwise. - */ -export function isPotentialInfiniteLoop(error: any) { - if (error instanceof TimeoutError) { - return true - } else if (error instanceof ExceptionError) { - const message = error.explain() - for (const toMatch of Object.values(StackOverflowMessages)) { - if (message.includes(toMatch)) { - return true - } - } - } - return false -} - -export enum InfiniteLoopErrorType { - NoBaseCase, - Cycle, - FromSmt -} - -export class InfiniteLoopError extends RuntimeSourceError { - public infiniteLoopType: InfiniteLoopErrorType - public message: string - public functionName: string | undefined - public streamMode: boolean - public codeStack: string[] - constructor( - functionName: string | undefined, - streamMode: boolean, - message: string, - infiniteLoopType: InfiniteLoopErrorType - ) { - super() - this.message = message - this.infiniteLoopType = infiniteLoopType - this.functionName = functionName - this.streamMode = streamMode - } - public explain() { - const entityName = this.functionName ? `function ${getOriginalName(this.functionName)}` : 'loop' - return this.streamMode - ? `The error may have arisen from forcing the infinite stream: ${entityName}.` - : `The ${entityName} has encountered an infinite loop. ` + this.message - } -} - -/** - * Determines whether the error is an infinite loop, and returns a tuple of - * [error type, is stream, error message, previous code]. - * * - * @param {Context} - The context being used. - * - * @returns [error type, is stream, error message, previous programs] if the error was an infinite loop - * @returns {undefined} otherwise - */ -export function getInfiniteLoopData( - context: Context -): undefined | [InfiniteLoopErrorType, boolean, string, es.Program[]] { - // return error type/string, prevCodeStack - // cast as any to access infiniteLoopError property later - const errors = context.errors - let latestError: any = errors[errors.length - 1] - if (latestError instanceof ExceptionError) { - latestError = latestError.error - } - let infiniteLoopError - if (latestError instanceof InfiniteLoopError) { - infiniteLoopError = latestError - } else if (latestError.hasOwnProperty('infiniteLoopError')) { - infiniteLoopError = latestError.infiniteLoopError as InfiniteLoopError - } - if (infiniteLoopError) { - return [ - infiniteLoopError.infiniteLoopType, - infiniteLoopError.streamMode, - infiniteLoopError.explain(), - context.previousPrograms - ] - } else { - return undefined - } -} diff --git a/src/infiniteLoops/instrument.ts b/src/infiniteLoops/instrument.ts deleted file mode 100644 index acd6c7354..000000000 --- a/src/infiniteLoops/instrument.ts +++ /dev/null @@ -1,646 +0,0 @@ -import { generate } from 'astring' -import type es from 'estree' - -import { transformImportDeclarations } from '../transpiler/transpiler' -import type { Node } from '../types' -import * as create from '../utils/ast/astCreator' -import { recursive, simple, WalkerCallback } from '../utils/walkers' -import { getIdsFromDeclaration } from '../utils/ast/helpers' -// transforms AST of program - -const globalIds = { - builtinsId: 'builtins', - functionsId: '__InfLoopFns', - stateId: '__InfLoopState', - modulesId: '__modules' -} - -enum FunctionNames { - nothingFunction, - concretize, - hybridize, - wrapArg, - dummify, - saveBool, - saveVar, - preFunction, - returnFunction, - postLoop, - enterLoop, - exitLoop, - trackLoc, - evalB, - evalU -} - -/** - * Renames all variables in the program to differentiate shadowed variables and - * variables declared with the same name but in different scopes. - * - * E.g. "function f(f)..." -> "function f_0(f_1)..." - * @param predefined A table of [key: string, value:string], where variables named 'key' will be renamed to 'value' - */ -function unshadowVariables(program: Node, predefined = {}) { - for (const name of Object.values(globalIds)) { - predefined[name] = name - } - const seenIds = new Set() - const env = [predefined] - const genId = (name: string) => { - let count = 0 - while (seenIds.has(`${name}_${count}`)) count++ - const newName = `${name}_${count}` - seenIds.add(newName) - env[0][name] = newName - return newName - } - const unshadowFunctionInner = ( - node: es.FunctionDeclaration | es.ArrowFunctionExpression | es.FunctionExpression, - s: undefined, - callback: WalkerCallback - ) => { - env.unshift({ ...env[0] }) - for (const id of node.params as es.Identifier[]) { - id.name = genId(id.name) - } - callback(node.body, undefined) - env.shift() - } - const doStatements = (stmts: es.Statement[], callback: WalkerCallback) => { - for (const stmt of stmts) { - if (stmt.type === 'FunctionDeclaration') { - // do hoisting first - if (stmt.id === null) { - throw new Error( - 'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.' - ) - } - stmt.id.name = genId(stmt.id.name) - } else if (stmt.type === 'VariableDeclaration') { - for (const decl of stmt.declarations) { - decl.id = decl.id as es.Identifier - const newName = genId(decl.id.name) - decl.id.name = newName - } - } - } - for (const stmt of stmts) { - callback(stmt, undefined) - } - } - recursive(program, [{}], { - BlockStatement(node: es.BlockStatement, s: undefined, callback: WalkerCallback) { - env.unshift({ ...env[0] }) - doStatements(node.body, callback) - env.shift() - }, - VariableDeclarator( - node: es.VariableDeclarator, - s: undefined, - callback: WalkerCallback - ) { - node.id = node.id as es.Identifier - if (node.init) { - callback(node.init, s) - } - }, - FunctionDeclaration( - node: es.FunctionDeclaration, - s: undefined, - callback: WalkerCallback - ) { - // note: params can shadow function name - env.unshift({ ...env[0] }) - for (const id of node.params as es.Identifier[]) { - id.name = genId(id.name) - } - callback(node.body, undefined) - env.shift() - }, - ForStatement(node: es.ForStatement, s: undefined, callback: WalkerCallback) { - env.unshift({ ...env[0] }) - if (node.init?.type === 'VariableDeclaration') doStatements([node.init], callback) - if (node.test) callback(node.test, s) - if (node.update) callback(node.update, s) - callback(node.body, s) - env.shift() - }, - ArrowFunctionExpression: unshadowFunctionInner, - FunctionExpression: unshadowFunctionInner, - Identifier(node: es.Identifier, _s: undefined, _callback: WalkerCallback) { - if (env[0][node.name]) { - node.name = env[0][node.name] - } else { - create.mutateToMemberExpression( - node, - create.identifier(globalIds.functionsId), - create.literal(FunctionNames.nothingFunction) - ) - ;(node as any).computed = true - } - }, - AssignmentExpression( - node: es.AssignmentExpression, - s: undefined, - callback: WalkerCallback - ) { - callback(node.left, s) - callback(node.right, s) - }, - TryStatement(node: es.TryStatement, s: undefined, callback: WalkerCallback) { - if (!node.finalizer) return // should not happen - env.unshift({ ...env[0] }) - doStatements(node.block.body, callback) - doStatements(node.finalizer.body, callback) - env.shift() - } - }) -} - -/** - * Returns the original name of the variable before - * it was changed during the code instrumentation process. - */ -export function getOriginalName(name: string) { - if (/^anon[0-9]+$/.exec(name)) { - return '(anonymous)' - } - let cutAt = name.length - 1 - while (name.charAt(cutAt) !== '_') { - cutAt-- - if (cutAt < 0) return '(error)' - } - return name.slice(0, cutAt) -} - -function callFunction(fun: FunctionNames) { - return create.memberExpression(create.identifier(globalIds.functionsId), fun) -} - -/** - * Wrap each argument in every call expression. - * - * E.g. "f(x,y)" -> "f(wrap(x), wrap(y))". - * Ensures we do not test functions passed as arguments - * for infinite loops. - */ -function wrapCallArguments(program: es.Program) { - simple(program, { - CallExpression(node: es.CallExpression) { - if (node.callee.type === 'MemberExpression') return - for (const arg of node.arguments) { - create.mutateToCallExpression(arg, callFunction(FunctionNames.wrapArg), [ - { ...(arg as es.Expression) }, - create.identifier(globalIds.stateId) - ]) - } - } - }) -} - -/** - * Turn all "is_null(x)" calls to "is_null(x, stateId)" to - * facilitate checking of infinite streams in stream mode. - */ -function addStateToIsNull(program: es.Program) { - simple(program, { - CallExpression(node: es.CallExpression) { - if (node.callee.type === 'Identifier' && node.callee.name === 'is_null_0') { - node.arguments.push(create.identifier(globalIds.stateId)) - } - } - }) -} - -/** - * Changes logical expressions to the corresponding conditional. - * Reduces the number of types of expressions we have to consider - * for the rest of the code transformations. - * - * E.g. "x && y" -> "x ? y : false" - */ -function transformLogicalExpressions(program: es.Program) { - simple(program, { - LogicalExpression(node: es.LogicalExpression) { - if (node.operator === '&&') { - create.mutateToConditionalExpression(node, node.left, node.right, create.literal(false)) - } else { - create.mutateToConditionalExpression(node, node.left, create.literal(true), node.right) - } - } - }) -} - -/** - * Changes -ary operations to functions that accept hybrid values as arguments. - * E.g. "1+1" -> "functions.evalB('+',1,1)" - */ -function hybridizeBinaryUnaryOperations(program: Node) { - simple(program, { - BinaryExpression(node: es.BinaryExpression) { - const { operator, left, right } = node - create.mutateToCallExpression(node, callFunction(FunctionNames.evalB), [ - create.literal(operator), - left, - right - ]) - }, - UnaryExpression(node: es.UnaryExpression) { - const { operator, argument } = node as es.UnaryExpression - create.mutateToCallExpression(node, callFunction(FunctionNames.evalU), [ - create.literal(operator), - argument - ]) - } - }) -} - -function hybridizeVariablesAndLiterals(program: Node) { - recursive(program, true, { - Identifier(node: es.Identifier, state: boolean, _callback: WalkerCallback) { - if (state) { - create.mutateToCallExpression(node, callFunction(FunctionNames.hybridize), [ - create.identifier(node.name), - create.literal(node.name), - create.identifier(globalIds.stateId) - ]) - } - }, - Literal(node: es.Literal, state: boolean, _callback: WalkerCallback) { - if (state && (typeof node.value === 'boolean' || typeof node.value === 'number')) { - create.mutateToCallExpression(node, callFunction(FunctionNames.dummify), [ - create.literal(node.value) - ]) - } - }, - CallExpression(node: es.CallExpression, state: boolean, callback: WalkerCallback) { - // ignore callee - for (const arg of node.arguments) { - callback(arg, state) - } - }, - MemberExpression(node: es.MemberExpression, state: boolean, callback: WalkerCallback) { - if (!node.computed) return - callback(node.object, false) - callback(node.property, false) - create.mutateToCallExpression(node.object, callFunction(FunctionNames.concretize), [ - { ...node.object } as es.Expression - ]) - create.mutateToCallExpression(node.property, callFunction(FunctionNames.concretize), [ - { ...node.property } as es.Expression - ]) - } - }) -} - -/** - * Wraps the RHS of variable assignment with a function to track it. - * E.g. "x = x + 1;" -> "x = saveVar(x + 1, 'x', state)". - * saveVar should return the result of "x + 1". - * - * For assignments to elements of arrays we concretize the RHS. - * E.g. "a[1] = y;" -> "a[1] = concretize(y);" - */ -function trackVariableAssignment(program: Node) { - simple(program, { - AssignmentExpression(node: es.AssignmentExpression) { - if (node.left.type === 'Identifier') { - node.right = create.callExpression(callFunction(FunctionNames.saveVar), [ - node.right, - create.literal(node.left.name), - create.identifier(globalIds.stateId) - ]) - } else if (node.left.type === 'MemberExpression') { - node.right = create.callExpression(callFunction(FunctionNames.concretize), [ - { ...node.right } - ]) - } - } - }) -} - -/** - * Replaces the test of the node with a function to track the result in the state. - * - * E.g. "x===0 ? 1 : 0;" -> "saveBool(x === 0, state) ? 1 : 0;". - * saveBool should return the result of "x === 0" - */ -function saveTheTest( - node: es.IfStatement | es.ConditionalExpression | es.WhileStatement | es.ForStatement -) { - if (node.test === null || node.test === undefined) { - return - } - const newTest = create.callExpression(callFunction(FunctionNames.saveBool), [ - node.test, - create.identifier(globalIds.stateId) - ]) - node.test = newTest -} - -/** - * Mutates a node in-place, turning it into a block statement. - * @param node Node to mutate. - * @param prepend Optional statement to prepend in the result. - * @param append Optional statement to append in the result. - */ -function inPlaceEnclose(node: es.Statement, prepend?: es.Statement, append?: es.Statement) { - const shallowCopy = { ...node } - node.type = 'BlockStatement' - node = node as es.BlockStatement - node.body = [shallowCopy] - if (prepend !== undefined) { - node.body.unshift(prepend) - } - if (append !== undefined) { - node.body.push(append) - } -} - -/** - * Add tracking to if statements and conditional expressions in the state using saveTheTest. - */ -function trackIfStatements(program: Node) { - const theFunction = (node: es.IfStatement | es.ConditionalExpression) => saveTheTest(node) - simple(program, { IfStatement: theFunction, ConditionalExpression: theFunction }) -} - -/** - * Tracks loop iterations by adding saveTheTest, postLoop functions. - * postLoop will be executed after the body (and the update if it is a for loop). - * Also adds enter/exitLoop before/after the loop. - * - * E.g. "for(let i=0;i<10;i=i+1) {display(i);}" - * -> "enterLoop(state); - * for(let i=0;i<10; postLoop(state, i=i+1)) {display(i);}; - * exitLoop(state);" - * Where postLoop should return the value of its (optional) second argument. - */ -function trackLoops(program: Node) { - const makeCallStatement = (name: FunctionNames, args: es.Expression[]) => - create.expressionStatement(create.callExpression(callFunction(name), args)) - const stateExpr = create.identifier(globalIds.stateId) - simple(program, { - WhileStatement: (node: es.WhileStatement) => { - saveTheTest(node) - inPlaceEnclose(node.body, undefined, makeCallStatement(FunctionNames.postLoop, [stateExpr])) - inPlaceEnclose( - node, - makeCallStatement(FunctionNames.enterLoop, [stateExpr]), - makeCallStatement(FunctionNames.exitLoop, [stateExpr]) - ) - }, - ForStatement: (node: es.ForStatement) => { - saveTheTest(node) - const theUpdate = node.update ? node.update : create.identifier('undefined') - node.update = create.callExpression(callFunction(FunctionNames.postLoop), [ - stateExpr, - theUpdate - ]) - inPlaceEnclose( - node, - makeCallStatement(FunctionNames.enterLoop, [stateExpr]), - makeCallStatement(FunctionNames.exitLoop, [stateExpr]) - ) - } - }) -} - -/** - * Tracks function iterations by adding preFunction and returnFunction functions. - * preFunction is prepended to every function body, and returnFunction is used to - * wrap the argument of return statements. - * - * E.g. "function f(x) {return x;}" - * -> "function f(x) { - * preFunction('f',[x], state); - * return returnFunction(x, state); - * }" - * where returnFunction should return its first argument 'x'. - */ -function trackFunctions(program: Node) { - const preFunction = (name: string, params: es.Pattern[]) => { - const args = params - .filter(x => x.type === 'Identifier') - .map(x => (x as es.Identifier).name) - .map(x => create.arrayExpression([create.literal(x), create.identifier(x)])) - - return create.expressionStatement( - create.callExpression(callFunction(FunctionNames.preFunction), [ - create.literal(name), - create.arrayExpression(args), - create.identifier(globalIds.stateId) - ]) - ) - } - - let counter = 0 - const anonFunction = (node: es.ArrowFunctionExpression | es.FunctionExpression) => { - if (node.body.type !== 'BlockStatement') { - create.mutateToReturnStatement(node.body, { ...node.body }) - } - inPlaceEnclose(node.body as es.Statement, preFunction(`anon${counter++}`, node.params)) - } - simple(program, { - ArrowFunctionExpression: anonFunction, - FunctionExpression: anonFunction, - FunctionDeclaration(node: es.FunctionDeclaration) { - if (node.id === null) { - throw new Error( - 'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.' - ) - } - const name = node.id.name - inPlaceEnclose(node.body, preFunction(name, node.params)) - } - }) - simple(program, { - ReturnStatement(node: es.ReturnStatement) { - const hasNoArgs = node.argument === null || node.argument === undefined - const arg = hasNoArgs ? create.identifier('undefined') : (node.argument as es.Expression) - const argsForCall = [arg, create.identifier(globalIds.stateId)] - node.argument = create.callExpression(callFunction(FunctionNames.returnFunction), argsForCall) - } - }) -} - -function builtinsToStmts(builtins: Iterable) { - const makeDecl = (name: string) => - create.declaration( - name, - 'const', - create.callExpression( - create.memberExpression(create.identifier(globalIds.builtinsId), 'get'), - [create.literal(name)] - ) - ) - return [...builtins].map(makeDecl) -} - -/** - * Make all variables in the 'try' block function-scoped so they - * can be accessed in the 'finally' block - */ -function toVarDeclaration(stmt: es.Statement) { - simple(stmt, { - VariableDeclaration(node: es.VariableDeclaration) { - node.kind = 'var' - } - }) -} - -/** - * There may have been other programs run in the REPL. This hack - * 'combines' the other programs and the current program into a single - * large program by enclosing the past programs in 'try' blocks, and the - * current program in a 'finally' block. Any errors (including detected - * infinite loops) in the past code will be ignored in the empty 'catch' - * block. - */ -function wrapOldCode(current: es.Program, toWrap: es.Statement[]) { - for (const stmt of toWrap) { - toVarDeclaration(stmt) - } - const tryStmt: es.TryStatement = { - type: 'TryStatement', - block: create.blockStatement([...toWrap]), - handler: { - type: 'CatchClause', - param: create.identifier('e'), - body: create.blockStatement([]) - }, - finalizer: create.blockStatement([...(current.body as es.Statement[])]) - } - current.body = [tryStmt] -} - -function makePositions(position: es.Position) { - return create.objectExpression([ - create.property('line', create.literal(position.line)), - create.property('column', create.literal(position.column)) - ]) -} - -function savePositionAsExpression(loc: es.SourceLocation | undefined | null) { - if (loc !== undefined && loc !== null) { - return create.objectExpression([ - create.property('start', makePositions(loc.start)), - create.property('end', makePositions(loc.end)) - ]) - } else { - return create.identifier('undefined') - } -} - -/** - * Wraps every callExpression and prepends every loop body - * with a function that saves the callExpression/loop's SourceLocation - * (line number etc) in the state. This location will be used in the - * error given to the user. - * - * E.g. "f(x);" -> "trackLoc({position object}, state, ()=>f(x))". - * where trackLoc should return the result of "(()=>f(x))();". - */ -function trackLocations(program: es.Program) { - // Note: only add locations for most recently entered code - const trackerFn = callFunction(FunctionNames.trackLoc) - const stateExpr = create.identifier(globalIds.stateId) - const doLoops = ( - node: es.ForStatement | es.WhileStatement, - _state: undefined, - _callback: WalkerCallback - ) => { - inPlaceEnclose( - node.body, - create.expressionStatement( - create.callExpression(trackerFn, [savePositionAsExpression(node.loc), stateExpr]) - ) - ) - } - recursive(program, undefined, { - CallExpression( - node: es.CallExpression, - _state: undefined, - _callback: WalkerCallback - ) { - if (node.callee.type === 'MemberExpression') return - const copy: es.CallExpression = { ...node } - const lazyCall = create.arrowFunctionExpression([], copy) - create.mutateToCallExpression(node, trackerFn, [ - savePositionAsExpression(node.loc), - stateExpr, - lazyCall - ]) - }, - ForStatement: doLoops, - WhileStatement: doLoops - }) -} - -function handleImports(programs: es.Program[]): string[] { - const imports = programs.flatMap(program => { - const [importsToAdd, otherNodes] = transformImportDeclarations( - program, - create.identifier(globalIds.modulesId) - ) - program.body = [...importsToAdd, ...otherNodes] - return importsToAdd.flatMap(decl => { - const ids = getIdsFromDeclaration(decl) - return ids.map(id => id.name) - }) - }) - - return [...new Set(imports)] -} - -/** - * Instruments the given code with functions that track the state of the program. - * - * @param previous programs that were previously executed in the REPL, most recent first (at ix 0). - * @param program most recent program executed. - * @param builtins Names of builtin functions. - * @returns code with instrumentations. - */ -function instrument( - previous: es.Program[], - program: es.Program, - builtins: Iterable -): string { - const { builtinsId, functionsId, stateId } = globalIds - const predefined = {} - predefined[builtinsId] = builtinsId - predefined[functionsId] = functionsId - predefined[stateId] = stateId - const innerProgram = { ...program } - - const moduleNames = handleImports([program].concat(previous)) - - for (const name of moduleNames) { - predefined[name] = name - } - for (const toWrap of previous) { - wrapOldCode(program, toWrap.body as es.Statement[]) - } - wrapOldCode(program, builtinsToStmts(builtins)) - unshadowVariables(program, predefined) - transformLogicalExpressions(program) - hybridizeBinaryUnaryOperations(program) - hybridizeVariablesAndLiterals(program) - // tracking functions: add functions to record runtime data. - - trackVariableAssignment(program) - trackIfStatements(program) - trackLoops(program) - trackFunctions(program) - trackLocations(innerProgram) - addStateToIsNull(program) - wrapCallArguments(program) - - return generate(program) -} - -export { - instrument, - FunctionNames as InfiniteLoopRuntimeFunctions, - globalIds as InfiniteLoopRuntimeObjectNames -} diff --git a/src/infiniteLoops/runtime.ts b/src/infiniteLoops/runtime.ts deleted file mode 100644 index d125ebb12..000000000 --- a/src/infiniteLoops/runtime.ts +++ /dev/null @@ -1,348 +0,0 @@ -import type es from 'estree' - -import createContext from '../createContext' -import { parse } from '../parser/parser' -import * as stdList from '../stdlib/list' -import { Chapter, Variant, type NativeStorage } from '../types' -import * as create from '../utils/ast/astCreator' -import { checkForInfiniteLoop } from './detect' -import { InfiniteLoopError } from './errors' -import { - InfiniteLoopRuntimeFunctions as FunctionNames, - InfiniteLoopRuntimeObjectNames, - instrument -} from './instrument' -import * as st from './state' -import * as sym from './symbolic' - -function checkTimeout(state: st.State) { - if (state.hasTimedOut()) { - throw new Error('timeout') - } -} - -/** - * This function is run whenever a variable is being accessed. - * If a variable has been added to state.variablesToReset, it will - * be 'reset' (concretized and re-hybridized) here. - */ -function hybridize(originalValue: any, name: string, state: st.State) { - if (typeof originalValue === 'function') { - return originalValue - } - let value = originalValue - if (state.variablesToReset.has(name)) { - value = sym.deepConcretizeInplace(value) - } - return sym.hybridizeNamed(name, value) -} - -/** - * Function to keep track of assignment expressions. - */ -function saveVarIfHybrid(value: any, name: string, state: st.State) { - state.variablesToReset.delete(name) - if (sym.isHybrid(value)) { - state.variablesModified.set(name, value) - } - return value -} - -/** - * Saves the boolean value if it is a hybrid, else set the - * path to invalid. - * Does not save in the path if the value is a boolean literal. - */ -function saveBoolIfHybrid(value: any, state: st.State) { - if (sym.isHybrid(value) && value.type === 'value') { - if (value.validity !== sym.Validity.Valid) { - state.setInvalidPath() - return sym.shallowConcretize(value) - } - if (value.symbolic.type !== 'Literal') { - let theExpr: es.Expression = value.symbolic - if (!value.concrete) { - theExpr = value.negation ? value.negation : create.unaryExpression('!', theExpr) - } - state.savePath(theExpr) - } - return sym.shallowConcretize(value) - } else { - state.setInvalidPath() - return value - } -} - -/** - * If a function was passed as an argument we do not - * check it for infinite loops. Wraps those functions - * with a decorator that activates a flag in the state. - */ -function wrapArgIfFunction(arg: any, state: st.State) { - if (typeof arg === 'function') { - return (...args: any) => { - state.functionWasPassedAsArgument = true - return arg(...args) - } - } - return arg -} - -/** - * For higher-order functions, we add the names of its parameters - * that are functions to differentiate different combinations of - * function invocations + parameters. - * - * e.g. - * const f = x=>x; - * const g = x=>x+1; - * const h = f=>f(1); - * - * h(f) will have a different oracle name from h(g). - */ -function makeOracleName(name: string, args: [string, any][]) { - let result = name - for (const [n, v] of args) { - if (typeof v === 'function') { - result = `${result}_${n}:${v.name}` - } - } - return result -} - -function preFunction(name: string, args: [string, any][], state: st.State) { - checkTimeout(state) - // track functions which were passed as arguments in a different tracker - const newName = state.functionWasPassedAsArgument ? '*' + name : makeOracleName(name, args) - const [tracker, firstIteration] = state.enterFunction(newName) - if (!firstIteration) { - state.cleanUpVariables() - state.saveArgsInTransition(args, tracker) - if (!state.functionWasPassedAsArgument) { - const previousIterations = tracker.slice(0, tracker.length - 1) - checkForInfiniteLoopIfMeetsThreshold(previousIterations, state, name) - } - } - tracker.push(state.newStackFrame(newName)) - - // reset the flag - state.functionWasPassedAsArgument = false -} - -function returnFunction(value: any, state: st.State) { - state.cleanUpVariables() - if (!state.streamMode) state.returnLastFunction() - return value -} - -/** - * Executed before the loop is entered to create a new iteration - * tracker. - */ -function enterLoop(state: st.State) { - state.loopStack.unshift([state.newStackFrame('loopRoot')]) -} - -// ignoreMe: hack to squeeze this inside the 'update' of for statements -function postLoop(state: st.State, ignoreMe?: any) { - checkTimeout(state) - const previousIterations = state.loopStack[0] - checkForInfiniteLoopIfMeetsThreshold( - previousIterations.slice(0, previousIterations.length - 1), - state - ) - state.cleanUpVariables() - previousIterations.push(state.newStackFrame('loop')) - return ignoreMe -} - -/** - * Always executed after a loop terminates, or breaks, to clean up - * variables and pop the last iteration tracker. - */ -function exitLoop(state: st.State) { - state.cleanUpVariables() - state.exitLoop() -} - -/** - * If the number of iterations (given by the length - * of stackPositions) is equal to a power of 2 times - * the threshold, check these iterations for infinite loop. - */ -function checkForInfiniteLoopIfMeetsThreshold( - stackPositions: number[], - state: st.State, - functionName?: string -) { - let checkpoint = state.threshold - while (checkpoint <= stackPositions.length) { - if (stackPositions.length === checkpoint) { - checkForInfiniteLoop(stackPositions, state, functionName) - } - checkpoint = checkpoint * 2 - } -} - -/** - * Test if stream is infinite. May destructively change the program - * environment. If it is not infinite, throw a timeout error. - */ -function testIfInfiniteStream(stream: any, state: st.State) { - let next = stream - for (let i = 0; i <= state.threshold; i++) { - if (stdList.is_null(next)) { - break - } else { - const nextTail = stdList.is_pair(next) ? next[1] : undefined - if (typeof nextTail === 'function') { - next = sym.shallowConcretize(nextTail()) - } else { - break - } - } - } - throw new Error('timeout') -} - -const builtinSpecialCases = { - is_null(maybeHybrid: any, state?: st.State) { - const xs = sym.shallowConcretize(maybeHybrid) - const conc = stdList.is_null(xs) - const theTail = stdList.is_pair(xs) ? xs[1] : undefined - const isStream = typeof theTail === 'function' - if (state && isStream) { - const lastFunction = state.getLastFunctionName() - if (state.streamMode === true && state.streamLastFunction === lastFunction) { - // heuristic to make sure we are at the same is_null call - testIfInfiniteStream(sym.shallowConcretize(theTail()), state) - } else { - let count = state.streamCounts.get(lastFunction) - if (count === undefined) { - count = 1 - } - if (count > state.streamThreshold) { - state.streamMode = true - state.streamLastFunction = lastFunction - } - state.streamCounts.set(lastFunction, count + 1) - } - } else { - return conc - } - return - }, - // mimic behaviour without printing - display: (...x: any[]) => x[0], - display_list: (...x: any[]) => x[0] -} - -function returnInvalidIfNumeric(val: any, validity = sym.Validity.NoSmt) { - if (typeof val === 'number') { - const result = sym.makeDummyHybrid(val) - result.validity = validity - return result - } else { - return val - } -} - -function prepareBuiltins(oldBuiltins: Map) { - const nonDetFunctions = ['get_time', 'math_random'] - const newBuiltins = new Map() - for (const [name, fun] of oldBuiltins) { - const specialCase = builtinSpecialCases[name] - if (specialCase !== undefined) { - newBuiltins.set(name, specialCase) - } else { - const functionValidity = nonDetFunctions.includes(name) - ? sym.Validity.NoCycle - : sym.Validity.NoSmt - newBuiltins.set(name, (...args: any[]) => { - const validityOfArgs = args.filter(sym.isHybrid).map(x => x.validity) - const mostInvalid = Math.max(functionValidity, ...validityOfArgs) - return returnInvalidIfNumeric(fun(...args.map(sym.shallowConcretize)), mostInvalid) - }) - } - } - newBuiltins.set('undefined', undefined) - return newBuiltins -} - -function nothingFunction(..._args: any[]) { - return nothingFunction -} - -function trackLoc(loc: es.SourceLocation | undefined, state: st.State, ignoreMe?: () => any) { - state.lastLocation = loc - if (ignoreMe !== undefined) { - return ignoreMe() - } -} - -const functions = { - [FunctionNames.nothingFunction]: nothingFunction, - [FunctionNames.concretize]: sym.shallowConcretize, - [FunctionNames.hybridize]: hybridize, - [FunctionNames.wrapArg]: wrapArgIfFunction, - [FunctionNames.dummify]: sym.makeDummyHybrid, - [FunctionNames.saveBool]: saveBoolIfHybrid, - [FunctionNames.saveVar]: saveVarIfHybrid, - [FunctionNames.preFunction]: preFunction, - [FunctionNames.returnFunction]: returnFunction, - [FunctionNames.postLoop]: postLoop, - [FunctionNames.enterLoop]: enterLoop, - [FunctionNames.exitLoop]: exitLoop, - [FunctionNames.trackLoc]: trackLoc, - [FunctionNames.evalB]: sym.evaluateHybridBinary, - [FunctionNames.evalU]: sym.evaluateHybridUnary -} - -/** - * Tests the given program for infinite loops. - * @param program Program to test. - * @param previousProgramsStack Any code previously entered in the REPL & parsed into AST. - * @returns SourceError if an infinite loop was detected, undefined otherwise. - */ -export function testForInfiniteLoop( - program: es.Program, - previousProgramsStack: es.Program[], - loadedModules: NativeStorage['loadedModules'] -) { - const context = createContext(Chapter.SOURCE_4, Variant.DEFAULT, undefined, undefined) - const prelude = parse(context.prelude as string, context) as es.Program - context.prelude = null - const previous: es.Program[] = [...previousProgramsStack, prelude] - const newBuiltins = prepareBuiltins(context.nativeStorage.builtins) - const { builtinsId, functionsId, stateId, modulesId } = InfiniteLoopRuntimeObjectNames - - const instrumentedCode = instrument(previous, program, newBuiltins.keys()) - const state = new st.State() - - const sandboxedRun = new Function( - 'code', - functionsId, - stateId, - builtinsId, - modulesId, - // redeclare window so modules don't do anything funny like play sounds - '{let window = {}; return eval(code)}' - ) - - try { - sandboxedRun(instrumentedCode, functions, state, newBuiltins, loadedModules) - } catch (error) { - if (error instanceof InfiniteLoopError) { - if (state.lastLocation !== undefined) { - error.location = state.lastLocation - } - return error - } - // Programs that exceed the maximum call stack size are okay as long as they terminate. - if (error instanceof RangeError && error.message === 'Maximum call stack size exceeded') { - return undefined - } - throw error - } - return undefined -} diff --git a/src/infiniteLoops/state.ts b/src/infiniteLoops/state.ts deleted file mode 100644 index a48c49cf4..000000000 --- a/src/infiniteLoops/state.ts +++ /dev/null @@ -1,239 +0,0 @@ -import { generate } from 'astring' -import * as es from 'estree' - -import { identifier } from '../utils/ast/astCreator' -import * as sym from './symbolic' - -// Object + functions called during runtime to check for infinite loops - -type Path = number[] -export type Transition = { - name: string - value: any - cachedSymbolicValue: number -} -const makeTransition = (name: string, value: any, id: number) => - ({ name: name, value: value, cachedSymbolicValue: id }) as Transition -type FunctionStackFrame = { - name: string - transitions: Transition[] -} -const makeFunctionStackFrame = (name: string, transitions: Transition[]) => - ({ name: name, transitions: transitions }) as FunctionStackFrame -type Iteration = { - loc: string - paths: Path - transitions: Transition[] -} -type IterationsTracker = number[] -const noSmtTransitionId = -1 -const nonDetTransitionId = -2 - -export class State { - variablesModified: Map - variablesToReset: Set - stringToIdCache: Map - idToStringCache: string[] - idToExprCache: es.Expression[] - mixedStack: Iteration[] - stackPointer: number - loopStack: IterationsTracker[] - functionTrackers: Map - functionStack: FunctionStackFrame[] - threshold: number - streamThreshold: number - startTime: number - timeout: number - streamMode: boolean - streamLastFunction: string | undefined - streamCounts: Map - lastLocation: es.SourceLocation | undefined - functionWasPassedAsArgument: boolean - constructor(timeout = 4000, threshold = 20, streamThreshold = threshold * 2) { - // arbitrary defaults - this.variablesModified = new Map() - this.variablesToReset = new Set() - this.stringToIdCache = new Map() - this.idToStringCache = [] - this.idToExprCache = [] - this.mixedStack = [{ loc: '(ROOT)', paths: [], transitions: [] }] - this.stackPointer = 0 - this.loopStack = [] - this.functionTrackers = new Map() - this.functionStack = [] - this.threshold = threshold - this.streamThreshold = streamThreshold - this.startTime = Date.now() - this.timeout = timeout - this.streamMode = false - this.streamLastFunction = undefined - this.streamCounts = new Map() - this.functionWasPassedAsArgument = false - } - static isInvalidPath(path: Path) { - return path.length === 1 && path[0] === -1 - } - static isNonDetTransition(transition: Transition[]) { - return transition.some(x => x.cachedSymbolicValue === nonDetTransitionId) - } - static isInvalidTransition(transition: Transition[]) { - return ( - State.isNonDetTransition(transition) || - transition.some(x => x.cachedSymbolicValue === noSmtTransitionId) - ) - } - /** - * Takes in an expression and returns its cached representation. - */ - public toCached(expr: es.Expression) { - const asString = generate(expr) - const item = this.stringToIdCache.get(asString) - if (item === undefined) { - const id = this.stringToIdCache.size - this.stringToIdCache.set(asString, id) - this.idToExprCache[id] = expr - this.idToStringCache[id] = asString - return id - } else { - return item - } - } - public popStackToStackPointer() { - if (this.mixedStack.length !== this.stackPointer) { - this.mixedStack = this.mixedStack.slice(0, this.stackPointer + 1) - } - } - public exitLoop() { - const tracker = this.loopStack[0] - const lastPosn = tracker.pop() - if (lastPosn !== undefined) { - this.stackPointer = lastPosn - 1 - } - this.loopStack.shift() - this.popStackToStackPointer() - } - - public savePath(expr: es.Expression) { - const currentPath = this.mixedStack[this.stackPointer].paths - if (!State.isInvalidPath(currentPath)) { - const id = this.toCached(expr) - currentPath.push(id) - } - } - /** - * Sets the current path as invalid. - */ - public setInvalidPath() { - this.mixedStack[this.stackPointer].paths = [-1] - } - public saveTransition(name: string, value: sym.Hybrid) { - const concrete = value.concrete - let id - if (value.validity === sym.Validity.Valid) { - id = this.toCached(value.symbolic) - } else if (value.validity === sym.Validity.NoSmt) { - id = noSmtTransitionId - } else { - id = nonDetTransitionId - } - const transitions = this.mixedStack[this.stackPointer].transitions - for (let i = 0; i < transitions.length; i++) { - const transition = transitions[i] - if (transition[0] === name) { - transition[1] = concrete - transition[2] = id - return - } - } - // no entry with the same name - transitions.push(makeTransition(name, concrete, id)) - } - /** - * Creates a new stack frame. - * @returns pointer to the new stack frame. - */ - public newStackFrame(loc: string) { - this.stackPointer++ - this.mixedStack.push({ loc: loc, paths: [], transitions: [] }) - return this.stackPointer - } - /** - * Saves variables that were modified to the current transition. - * Also adds the variable to this.variablesToReset. These variables - * will be lazily reset (concretized and re-hybridized) in runtime.hybridize. - */ - public cleanUpVariables() { - for (const [name, value] of this.variablesModified) { - this.saveTransition(name, value) - this.variablesToReset.add(name) - } - } - /** - * Records entering a function in the state. - * @param name name of the function. - * @returns [tracker, firstIteration] where firstIteration is true if this is the functions first iteration. - */ - public enterFunction(name: string): [IterationsTracker, boolean] { - const transitions = this.mixedStack[this.stackPointer].transitions - this.functionStack.push(makeFunctionStackFrame(name, transitions)) - let tracker = this.functionTrackers.get(name) - let firstIteration = false - if (tracker === undefined) { - tracker = [] - this.functionTrackers.set(name, tracker) - firstIteration = true - } - firstIteration = tracker.length === 0 - return [tracker, firstIteration] - } - - /** - * Saves args into the last iteration's transition in the tracker. - */ - public saveArgsInTransition(args: any[], tracker: IterationsTracker) { - const transitions: Transition[] = [] - for (const [name, val] of args) { - if (sym.isHybrid(val)) { - if (val.validity === sym.Validity.Valid) { - transitions.push(makeTransition(name, val.concrete, this.toCached(val.symbolic))) - } else if (val.validity === sym.Validity.NoSmt) { - transitions.push(makeTransition(name, val.concrete, noSmtTransitionId)) - } else { - transitions.push(makeTransition(name, val.concrete, nonDetTransitionId)) - } - } else { - transitions.push(makeTransition(name, val, this.toCached(identifier('undefined')))) - } - this.variablesToReset.add(name) - } - const prevPointer = tracker[tracker.length - 1] - if (prevPointer > -1) { - this.mixedStack[prevPointer].transitions.push(...transitions) - } - } - - /** - * Records in the state that the last function has returned. - */ - public returnLastFunction() { - const lastFunctionFrame = this.functionStack.pop() as FunctionStackFrame - const tracker = this.functionTrackers.get(lastFunctionFrame.name) as IterationsTracker - const lastPosn = tracker.pop() - if (lastPosn !== undefined) { - this.stackPointer = lastPosn - 1 - } - this.popStackToStackPointer() - this.mixedStack[this.stackPointer].transitions = lastFunctionFrame.transitions - this.setInvalidPath() - } - - public hasTimedOut() { - return Date.now() - this.startTime > this.timeout - } - /** - * @returns the name of the last function in the stack. - */ - public getLastFunctionName() { - return this.functionStack[this.functionStack.length - 1][0] - } -} diff --git a/src/infiniteLoops/symbolic.ts b/src/infiniteLoops/symbolic.ts deleted file mode 100644 index 6673f8989..000000000 --- a/src/infiniteLoops/symbolic.ts +++ /dev/null @@ -1,246 +0,0 @@ -import * as es from 'estree' - -import * as create from '../utils/ast/astCreator' -import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators' - -// data structure for symbolic + hybrid values - -export enum Validity { - Valid, - NoSmt, - NoCycle -} - -function isInvalid(status: Validity) { - return status !== Validity.Valid -} - -export type HybridValue = { - type: 'value' - concrete: any - symbolic: es.Expression - negation?: es.Expression - validity: Validity -} - -export type HybridArray = { - type: 'array' - concrete: any - symbolic: es.Expression - validity: Validity -} - -export type Hybrid = HybridValue | HybridArray - -export function hybridizeNamed(name: string, value: any): Hybrid { - if (isHybrid(value) || value === undefined || typeof value === 'function') { - return value - } else if (Array.isArray(value)) { - return makeHybridArray(name, value) - } else { - return hybridValueConstructor(value, create.identifier(name)) - } -} - -export function isHybrid(value: any): value is Hybrid { - return typeof value === 'object' && value !== null && value.hasOwnProperty('symbolic') -} - -function isConcreteValue(value: any) { - return !(isHybrid(value) || Array.isArray(value)) -} - -export const hybridValueConstructor = ( - concrete: any, - symbolic: es.Expression, - validity = Validity.Valid -) => - ({ - type: 'value', - concrete: concrete, - symbolic: symbolic, - validity: validity - }) as HybridValue - -export function makeDummyHybrid(concrete: any): HybridValue { - if (!isConcreteValue(concrete)) { - return concrete - } - const val: HybridValue = { - type: 'value', - concrete: concrete, - symbolic: create.literal(concrete), - validity: Validity.Valid - } - return val -} - -export function getBooleanResult(value: HybridValue) { - if (value.concrete) { - return value.symbolic - } - if (value.negation !== undefined) { - return value.negation - } else { - return create.unaryExpression('!', value.symbolic) - } -} - -export const hybridArrayConstructor = ( - concrete: any, - symbolic: es.Expression, - listHeads = [] as HybridArray[] -) => - ({ - type: 'array', - concrete: concrete, - symbolic: symbolic, - listHeads: listHeads, - validity: Validity.Valid - }) as HybridArray - -function makeHybridArray(name: string, concrete: any[]): HybridArray { - // note single quotes used in generated indentifiers: quick hack to avoid name clashes - let count = 0 - const visited: any[][] = [] - function innerInplace(x: any[]) { - visited.push(x) - for (let i = 0; i < x.length; i++) { - if (Array.isArray(x[i])) { - let skip = false - for (const v of visited) { - if (x[i] === v) skip = true - } - if (!skip) innerInplace(x[i]) - } else if ( - x[i] !== null && - x[i] !== undefined && - x[i].symbolic === undefined && - typeof x[i] === 'number' - ) { - x[i] = hybridValueConstructor(x[i], create.identifier(`${name}'${count++}`)) - } - } - } - innerInplace(concrete) - // NOTE: below symbolic value won't be used in SMT - return hybridArrayConstructor(concrete, create.identifier(`${name}'array`)) -} - -export function deepConcretizeInplace(value: any) { - const seen = new WeakSet() - function innerInplace(x: any[]) { - seen.add(x) - for (let i = 0; i < x.length; i++) { - if (Array.isArray(x[i])) { - if (!seen.has(x[i])) { - innerInplace(x[i]) - } - } else { - x[i] = shallowConcretize(x[i]) - } - } - } - if (Array.isArray(value)) { - innerInplace(value) - return value - } else { - return shallowConcretize(value) - } -} - -export function shallowConcretize(value: any) { - if (isHybrid(value)) { - return value.concrete - } else { - return value - } -} - -function getAST(v: any): es.Expression { - if (isHybrid(v)) { - return v.symbolic - } else { - return create.literal(v) - } -} - -export function evaluateHybridBinary(op: es.BinaryOperator, lhs: any, rhs: any) { - if (Array.isArray(shallowConcretize(lhs)) || Array.isArray(shallowConcretize(rhs))) { - return hybridValueConstructor( - evaluateBinaryExpression(op, shallowConcretize(lhs), shallowConcretize(rhs)), - create.literal(false) - ) - } else if (isHybrid(lhs) || isHybrid(rhs)) { - const val = evaluateBinaryExpression(op, shallowConcretize(lhs), shallowConcretize(rhs)) - if (isInvalid(lhs.validity) || isInvalid(rhs.validity)) { - const result = makeDummyHybrid(val) - result.validity = Math.max(lhs.validity, rhs.validity) - return result - } - let res - if (op === '!==') { - res = hybridValueConstructor(val, neqRefine(lhs, rhs)) - } else { - res = hybridValueConstructor(val, create.binaryExpression(op, getAST(lhs), getAST(rhs))) - } - const neg = getNegation(op, lhs, rhs) - if (neg !== undefined) { - res.negation = neg - } - if (op === '!==' || op === '===') { - const concIsNumber = (x: any) => typeof shallowConcretize(x) === 'number' - if (!(concIsNumber(lhs) && concIsNumber(rhs))) { - res.validity = Validity.NoSmt - } - } - return res - } else { - return evaluateBinaryExpression(op, lhs, rhs) - } -} - -/** - * To provide more information to the SMT solver, whenever '!==' is encountered - * comparing 2 numbers, we replace it with '>' or '<' accordingly. - */ -function neqRefine(lhs: any, rhs: any) { - const op: es.BinaryOperator = shallowConcretize(lhs) < shallowConcretize(rhs) ? '<' : '>' - return create.binaryExpression(op, getAST(lhs), getAST(rhs)) -} - -function getNegation(op: es.BinaryOperator, lhs: any, rhs: any) { - const fromOp = ['>', '>=', '<', '<=', '!=='] - const toOp: es.BinaryOperator[] = ['<=', '<', '>=', '>', '==='] - const ix = fromOp.indexOf(op) - if (ix > -1) { - return create.binaryExpression(toOp[ix], getAST(lhs), getAST(rhs)) - } - if (op === '===') { - return neqRefine(lhs, rhs) - } - return undefined -} - -export function evaluateHybridUnary(op: es.UnaryOperator, val: any) { - if (isHybrid(val)) { - const conc = evaluateUnaryExpression(op, shallowConcretize(val)) - if (isInvalid(val.validity)) { - const result = makeDummyHybrid(val) - result.validity = val.validity - return result - } - if (val.symbolic.type === 'Literal') { - const newSym = { ...val.symbolic, val: conc } - return hybridValueConstructor(conc, newSym) - } else if (op === '!' && val.type === 'value' && val.negation !== undefined) { - const result = hybridValueConstructor(conc, val.negation) - result.negation = val.symbolic - return result - } else { - return hybridValueConstructor(conc, create.unaryExpression(op, getAST(val))) - } - } else { - return evaluateUnaryExpression(op, val) - } -} diff --git a/src/runner/sourceRunner.ts b/src/runner/sourceRunner.ts index 22c070d7d..08927fa29 100644 --- a/src/runner/sourceRunner.ts +++ b/src/runner/sourceRunner.ts @@ -6,11 +6,8 @@ import { CSEResultPromise, evaluate as CSEvaluate } from '../cse-machine/interpr import { ExceptionError } from '../errors/errors' import { RuntimeSourceError } from '../errors/runtimeSourceError' import { TimeoutError } from '../errors/timeoutErrors' -import { isPotentialInfiniteLoop } from '../infiniteLoops/errors' -import { testForInfiniteLoop } from '../infiniteLoops/runtime' import { sandboxedEval } from '../transpiler/evalContainer' import { transpile } from '../transpiler/transpiler' -import { Variant } from '../types' import { getSteps } from '../tracer/steppers' import { toSourceError } from './errors' import { resolvedErrorPromise } from './utils' @@ -65,25 +62,6 @@ const runners = { value } } catch (error) { - const isDefaultVariant = options.variant === undefined || options.variant === Variant.DEFAULT - if (isDefaultVariant && isPotentialInfiniteLoop(error)) { - const detectedInfiniteLoop = testForInfiniteLoop( - program, - context.previousPrograms.slice(1), - context.nativeStorage.loadedModules - ) - if (detectedInfiniteLoop !== undefined) { - if (options.throwInfiniteLoops) { - context.errors.push(detectedInfiniteLoop) - return resolvedErrorPromise - } else { - error.infiniteLoopError = detectedInfiniteLoop - if (error instanceof ExceptionError) { - ;(error.error as any).infiniteLoopError = detectedInfiniteLoop - } - } - } - } if (error instanceof RuntimeSourceError) { context.errors.push(error) if (error instanceof TimeoutError) { diff --git a/src/stdlib/__tests__/stream.ts b/src/stdlib/__tests__/stream.ts index e9529c811..1dce94bcb 100644 --- a/src/stdlib/__tests__/stream.ts +++ b/src/stdlib/__tests__/stream.ts @@ -33,9 +33,7 @@ describe('primitive stream functions', () => { stream_length(integers_from(0)); `, { chapter: Chapter.SOURCE_3 } - ).toMatchInlineSnapshot( - `"Line 1: The error may have arisen from forcing the infinite stream: function integers_from."` - ) + ).toContain(`RangeError: Maximum call stack size exceeded`) }, 15000) test('stream is properly created', () => {