UNPKG

js-slang

Version:

Javascript-based implementations of Source, written in Typescript

315 lines 11.6 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.testForInfiniteLoop = void 0; const createContext_1 = require("../createContext"); const parser_1 = require("../parser/parser"); const stdList = require("../stdlib/list"); const types_1 = require("../types"); const create = require("../utils/ast/astCreator"); const detect_1 = require("./detect"); const errors_1 = require("./errors"); const instrument_1 = require("./instrument"); const st = require("./state"); const sym = require("./symbolic"); function checkTimeout(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, name, 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, name, 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, 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 = 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, state) { if (typeof arg === 'function') { return (...args) => { 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, args) { let result = name; for (const [n, v] of args) { if (typeof v === 'function') { result = `${result}_${n}:${v.name}`; } } return result; } function preFunction(name, args, 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, 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) { state.loopStack.unshift([state.newStackFrame('loopRoot')]); } // ignoreMe: hack to squeeze this inside the 'update' of for statements function postLoop(state, ignoreMe) { 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) { 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, state, functionName) { let checkpoint = state.threshold; while (checkpoint <= stackPositions.length) { if (stackPositions.length === checkpoint) { (0, detect_1.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, 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, 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) => x[0], display_list: (...x) => x[0] }; function returnInvalidIfNumeric(val, validity = sym.Validity.NoSmt) { if (typeof val === 'number') { const result = sym.makeDummyHybrid(val); result.validity = validity; return result; } else { return val; } } function prepareBuiltins(oldBuiltins) { 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) => { 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) { return nothingFunction; } function trackLoc(loc, state, ignoreMe) { state.lastLocation = loc; if (ignoreMe !== undefined) { return ignoreMe(); } } const functions = { [instrument_1.InfiniteLoopRuntimeFunctions.nothingFunction]: nothingFunction, [instrument_1.InfiniteLoopRuntimeFunctions.concretize]: sym.shallowConcretize, [instrument_1.InfiniteLoopRuntimeFunctions.hybridize]: hybridize, [instrument_1.InfiniteLoopRuntimeFunctions.wrapArg]: wrapArgIfFunction, [instrument_1.InfiniteLoopRuntimeFunctions.dummify]: sym.makeDummyHybrid, [instrument_1.InfiniteLoopRuntimeFunctions.saveBool]: saveBoolIfHybrid, [instrument_1.InfiniteLoopRuntimeFunctions.saveVar]: saveVarIfHybrid, [instrument_1.InfiniteLoopRuntimeFunctions.preFunction]: preFunction, [instrument_1.InfiniteLoopRuntimeFunctions.returnFunction]: returnFunction, [instrument_1.InfiniteLoopRuntimeFunctions.postLoop]: postLoop, [instrument_1.InfiniteLoopRuntimeFunctions.enterLoop]: enterLoop, [instrument_1.InfiniteLoopRuntimeFunctions.exitLoop]: exitLoop, [instrument_1.InfiniteLoopRuntimeFunctions.trackLoc]: trackLoc, [instrument_1.InfiniteLoopRuntimeFunctions.evalB]: sym.evaluateHybridBinary, [instrument_1.InfiniteLoopRuntimeFunctions.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. */ function testForInfiniteLoop(program, previousProgramsStack, loadedModules) { const context = (0, createContext_1.default)(types_1.Chapter.SOURCE_4, types_1.Variant.DEFAULT, undefined, undefined); const prelude = (0, parser_1.parse)(context.prelude, context); context.prelude = null; const previous = [...previousProgramsStack, prelude]; const newBuiltins = prepareBuiltins(context.nativeStorage.builtins); const { builtinsId, functionsId, stateId, modulesId } = instrument_1.InfiniteLoopRuntimeObjectNames; const instrumentedCode = (0, instrument_1.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 errors_1.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; } exports.testForInfiniteLoop = testForInfiniteLoop; //# sourceMappingURL=runtime.js.map