From 6e8292cc390b60b140535b2ade5a8bfbf1353bf2 Mon Sep 17 00:00:00 2001 From: joeychensmart Date: Mon, 26 Jul 2021 14:46:55 +0800 Subject: [PATCH 1/2] Update infinite loop detector frontend --- src/commons/application/ApplicationTypes.ts | 2 + src/commons/application/types/SessionTypes.ts | 2 + src/commons/sagas/WorkspaceSaga.ts | 24 ++-- src/commons/sagas/__tests__/WorkspaceSaga.ts | 100 ++++++++++++--- src/commons/utils/InfiniteLoopReporter.ts | 118 +++--------------- 5 files changed, 120 insertions(+), 126 deletions(-) diff --git a/src/commons/application/ApplicationTypes.ts b/src/commons/application/ApplicationTypes.ts index f03781fa21..7bfcead0f2 100644 --- a/src/commons/application/ApplicationTypes.ts +++ b/src/commons/application/ApplicationTypes.ts @@ -287,6 +287,8 @@ export const defaultSession: SessionState = { accessToken: undefined, assessments: new Map(), assessmentOverviews: undefined, + experimentApproval: false, // TODO: get this from backend or sth + experimentCoinflip: Math.random() < 0.5, githubOctokitObject: { octokit: undefined }, grade: 0, gradingOverviews: undefined, diff --git a/src/commons/application/types/SessionTypes.ts b/src/commons/application/types/SessionTypes.ts index bf47d456b8..7cb174bcc1 100644 --- a/src/commons/application/types/SessionTypes.ts +++ b/src/commons/application/types/SessionTypes.ts @@ -48,6 +48,8 @@ export type SessionState = { readonly accessToken?: string; readonly assessmentOverviews?: AssessmentOverview[]; readonly assessments: Map; + readonly experimentApproval: boolean; + readonly experimentCoinflip: boolean; readonly grade: number; readonly gradingOverviews?: GradingOverview[]; readonly gradings: Map; diff --git a/src/commons/sagas/WorkspaceSaga.ts b/src/commons/sagas/WorkspaceSaga.ts index 6490ca5ca8..54bd215942 100644 --- a/src/commons/sagas/WorkspaceSaga.ts +++ b/src/commons/sagas/WorkspaceSaga.ts @@ -716,6 +716,9 @@ export function* evalCode( const isNonDet: boolean = context.variant === 'non-det'; const isLazy: boolean = context.variant === 'lazy'; const isWasm: boolean = context.variant === 'wasm'; + const throwInfiniteLoops: boolean = yield select( + (state: OverallState) => state.session.experimentCoinflip + ); const { result, interrupted, paused } = yield race({ result: actionType === DEBUG_RESUME @@ -726,6 +729,7 @@ export function* evalCode( scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: stepLimit, + throwInfiniteLoops: throwInfiniteLoops, useSubst: substActiveAndCorrectChapter }), @@ -776,13 +780,16 @@ export function* evalCode( context.errors = oldErrors; // for achievement event tracking const events = context.errors.length > 0 ? [EventType.ERROR] : []; + // report infinite loops but only for 'vanilla'/default source - if (context.variant === 'default') { - const infiniteLoopData = getInfiniteLoopData(context, code); + if (context.variant === undefined || context.variant === 'default') { + const infiniteLoopData = getInfiniteLoopData(context); if (infiniteLoopData) { - events.push(EventType.INFINITE_LOOP); - const [error, code] = infiniteLoopData; - yield call(reportInfiniteLoopError, error, code); + const approval = yield select((state: OverallState) => state.session.experimentApproval); + if (approval) { + events.push(EventType.INFINITE_LOOP); + yield call(reportInfiniteLoopError, ...infiniteLoopData); + } } } @@ -828,11 +835,14 @@ export function* evalTestCode( type: TestcaseType ) { yield put(actions.resetTestcase(workspaceLocation, index)); - + const throwInfiniteLoops: boolean = yield select( + (state: OverallState) => state.session.experimentCoinflip + ); const { result, interrupted } = yield race({ result: call(runInContext, code, context, { scheduler: 'preemptive', - originalMaxExecTime: execTime + originalMaxExecTime: execTime, + throwInfiniteLoops: throwInfiniteLoops }), /** * A BEGIN_INTERRUPT_EXECUTION signals the beginning of an interruption, diff --git a/src/commons/sagas/__tests__/WorkspaceSaga.ts b/src/commons/sagas/__tests__/WorkspaceSaga.ts index 82cf5d5849..9f468c8f05 100644 --- a/src/commons/sagas/__tests__/WorkspaceSaga.ts +++ b/src/commons/sagas/__tests__/WorkspaceSaga.ts @@ -28,7 +28,7 @@ import { import { Library, Testcase, TestcaseType, TestcaseTypes } from '../../assessment/AssessmentTypes'; import { mockRuntimeContext } from '../../mocks/ContextMocks'; import { mockTestcases } from '../../mocks/GradingMocks'; -import { reportInfiniteLoopError } from '../../utils/InfiniteLoopReporter'; +import { InfiniteLoopErrorType, reportInfiniteLoopError } from '../../utils/InfiniteLoopReporter'; import { showSuccessMessage, showWarningMessage } from '../../utils/NotificationsHelper'; import { beginClearContext, @@ -64,6 +64,11 @@ function generateDefaultState( ): OverallState { return { ...defaultState, + session: { + ...defaultState.session, + experimentApproval: false, + experimentCoinflip: true + }, workspaces: { ...defaultState.workspaces, [workspaceLocation]: { @@ -130,7 +135,8 @@ describe('EVAL_EDITOR', () => { scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true } ] }) @@ -148,7 +154,8 @@ describe('EVAL_EDITOR', () => { scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true } ] }) @@ -212,7 +219,8 @@ describe('EVAL_REPL', () => { scheduler: 'preemptive', originalMaxExecTime: 1000, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true }) .dispatch({ type: EVAL_REPL, @@ -641,7 +649,8 @@ describe('evalCode', () => { scheduler: 'preemptive', originalMaxExecTime: 1000, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true }; lastDebuggerResult = { status: 'error' }; state = generateDefaultState(workspaceLocation); @@ -656,7 +665,8 @@ describe('evalCode', () => { scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true }) .put(evalInterpreterSuccess(value, workspaceLocation)) .silentRun(); @@ -670,7 +680,8 @@ describe('evalCode', () => { scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true }) .put(endDebuggerPause(workspaceLocation)) .put(evalInterpreterSuccess('Breakpoint hit!', workspaceLocation)) @@ -684,7 +695,8 @@ describe('evalCode', () => { scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true }) .put.like({ action: { type: EVAL_INTERPRETER_ERROR } }) .silentRun(); @@ -707,17 +719,22 @@ describe('evalCode', () => { scheduler: 'preemptive', originalMaxExecTime: execTime, stepLimit: 1000, - useSubst: false + useSubst: false, + throwInfiniteLoops: true }) .put(evalInterpreterError(context.errors, workspaceLocation)) .silentRun(); }); test('calls reportInfiniteLoop on error and sends correct data to sentry', () => { - context = createContext(3); - const code1 = 'const test=[(x)=>x,2,3,[(x)=>x],5];function f(x){return f(x);}'; + state = { + ...state, + session: { ...state.session, experimentApproval: true, experimentCoinflip: true } + }; + const thisContext = createContext(3); + context = thisContext; + const code1 = 'function f(x){f(x);}'; const code2 = 'f(1);'; - state = generateDefaultState(workspaceLocation, {}); return runInContext(code1, context, { scheduler: 'preemptive', @@ -728,12 +745,64 @@ describe('evalCode', () => { .withState(state) .call( reportInfiniteLoopError, - 'source_protection_recursion', - 'function is_list(xs) {\n return is_null(xs) || is_pair(xs) && is_list(tail(xs));\n}\nfunction equal(xs, ys) {\n return is_pair(xs) ? is_pair(ys) && equal(head(xs), head(ys)) && equal(tail(xs), tail(ys)) : is_null(xs) ? is_null(ys) : is_number(xs) ? is_number(ys) && xs === ys : is_boolean(xs) ? is_boolean(ys) && (xs && ys || !xs && !ys) : is_string(xs) ? is_string(ys) && xs === ys : is_undefined(xs) ? is_undefined(ys) : is_function(ys) && xs === ys;\n}\nfunction $length(xs, acc) {\n return is_null(xs) ? acc : $length(tail(xs), acc + 1);\n}\nfunction length(xs) {\n return $length(xs, 0);\n}\nfunction $map(f, xs, acc) {\n return is_null(xs) ? reverse(acc) : $map(f, tail(xs), pair(f(head(xs)), acc));\n}\nfunction map(f, xs) {\n return $map(f, xs, null);\n}\nfunction $build_list(i, fun, already_built) {\n return i < 0 ? already_built : $build_list(i - 1, fun, pair(fun(i), already_built));\n}\nfunction build_list(fun, n) {\n return $build_list(n - 1, fun, null);\n}\nfunction for_each(fun, xs) {\n if (is_null(xs)) {\n return true;\n } else {\n fun(head(xs));\n return for_each(fun, tail(xs));\n }\n}\nfunction $list_to_string(xs, cont) {\n return is_null(xs) ? cont("null") : is_pair(xs) ? $list_to_string(head(xs), x => $list_to_string(tail(xs), y => cont("[" + x + "," + y + "]"))) : cont(stringify(xs));\n}\nfunction list_to_string(xs) {\n return $list_to_string(xs, x => x);\n}\nfunction $reverse(original, reversed) {\n return is_null(original) ? reversed : $reverse(tail(original), pair(head(original), reversed));\n}\nfunction reverse(xs) {\n return $reverse(xs, null);\n}\nfunction $append(xs, ys, cont) {\n return is_null(xs) ? cont(ys) : $append(tail(xs), ys, zs => cont(pair(head(xs), zs)));\n}\nfunction append(xs, ys) {\n return $append(xs, ys, xs => xs);\n}\nfunction member(v, xs) {\n return is_null(xs) ? null : v === head(xs) ? xs : member(v, tail(xs));\n}\nfunction $remove(v, xs, acc) {\n return is_null(xs) ? append(reverse(acc), xs) : v === head(xs) ? append(reverse(acc), tail(xs)) : $remove(v, tail(xs), pair(head(xs), acc));\n}\nfunction remove(v, xs) {\n return $remove(v, xs, null);\n}\nfunction $remove_all(v, xs, acc) {\n return is_null(xs) ? append(reverse(acc), xs) : v === head(xs) ? $remove_all(v, tail(xs), acc) : $remove_all(v, tail(xs), pair(head(xs), acc));\n}\nfunction remove_all(v, xs) {\n return $remove_all(v, xs, null);\n}\nfunction $filter(pred, xs, acc) {\n return is_null(xs) ? reverse(acc) : pred(head(xs)) ? $filter(pred, tail(xs), pair(head(xs), acc)) : $filter(pred, tail(xs), acc);\n}\nfunction filter(pred, xs) {\n return $filter(pred, xs, null);\n}\nfunction $enum_list(start, end, acc) {\n return start > end ? reverse(acc) : $enum_list(start + 1, end, pair(start, acc));\n}\nfunction enum_list(start, end) {\n return $enum_list(start, end, null);\n}\nfunction list_ref(xs, n) {\n return n === 0 ? head(xs) : list_ref(tail(xs), n - 1);\n}\nfunction $accumulate(f, initial, xs, cont) {\n return is_null(xs) ? cont(initial) : $accumulate(f, initial, tail(xs), x => cont(f(head(xs), x)));\n}\nfunction accumulate(f, initial, xs) {\n return $accumulate(f, initial, xs, x => x);\n}\nfunction is_stream(xs) {\n return is_null(xs) || is_pair(xs) && is_stream(stream_tail(xs));\n}\nfunction list_to_stream(xs) {\n return is_null(xs) ? null : pair(head(xs), () => list_to_stream(tail(xs)));\n}\nfunction stream_to_list(xs) {\n return is_null(xs) ? null : pair(head(xs), stream_to_list(stream_tail(xs)));\n}\nfunction stream_length(xs) {\n return is_null(xs) ? 0 : 1 + stream_length(stream_tail(xs));\n}\nfunction stream_map(f, s) {\n return is_null(s) ? null : pair(f(head(s)), () => stream_map(f, stream_tail(s)));\n}\nfunction build_stream(fun, n) {\n function build(i) {\n return i >= n ? null : pair(fun(i), () => build(i + 1));\n }\n return build(0);\n}\nfunction stream_for_each(fun, xs) {\n if (is_null(xs)) {\n return true;\n } else {\n fun(head(xs));\n return stream_for_each(fun, stream_tail(xs));\n }\n}\nfunction stream_reverse(xs) {\n function rev(original, reversed) {\n return is_null(original) ? reversed : rev(stream_tail(original), pair(head(original), () => reversed));\n }\n return rev(xs, null);\n}\nfunction stream_append(xs, ys) {\n return is_null(xs) ? ys : pair(head(xs), () => stream_append(stream_tail(xs), ys));\n}\nfunction stream_member(x, s) {\n return is_null(s) ? null : head(s) === x ? s : stream_member(x, stream_tail(s));\n}\nfunction stream_remove(v, xs) {\n return is_null(xs) ? null : v === head(xs) ? stream_tail(xs) : pair(head(xs), () => stream_remove(v, stream_tail(xs)));\n}\nfunction stream_remove_all(v, xs) {\n return is_null(xs) ? null : v === head(xs) ? stream_remove_all(v, stream_tail(xs)) : pair(head(xs), () => stream_remove_all(v, stream_tail(xs)));\n}\nfunction stream_filter(p, s) {\n return is_null(s) ? null : p(head(s)) ? pair(head(s), () => stream_filter(p, stream_tail(s))) : stream_filter(p, stream_tail(s));\n}\nfunction enum_stream(start, end) {\n return start > end ? null : pair(start, () => enum_stream(start + 1, end));\n}\nfunction integers_from(n) {\n return pair(n, () => integers_from(n + 1));\n}\nfunction eval_stream(s, n) {\n function es(s, n) {\n return n === 1 ? list(head(s)) : pair(head(s), es(stream_tail(s), n - 1));\n }\n return n === 0 ? null : es(s, n);\n}\nfunction stream_ref(s, n) {\n return n === 0 ? head(s) : stream_ref(stream_tail(s), n - 1);\n}\nconst test=[x => x,2,3,[x => x],5];\nfunction f(x) {\n return f(x);\n}' + InfiniteLoopErrorType.NoBaseCase, + false, + 'The function f has encountered an infinite loop. It has no base case.', + [code2, code1] ) .silentRun(); }); }); + + test('does not send correct data to sentry if approval is false', () => { + state = { + ...state, + session: { ...state.session, experimentApproval: false, experimentCoinflip: true } + }; + context = createContext(3); + const theCode = 'function f(x){f(x);} f(1);'; + + return expectSaga(evalCode, theCode, context, execTime, workspaceLocation, actionType) + .withState(state) + .not.call( + reportInfiniteLoopError, + InfiniteLoopErrorType.NoBaseCase, + false, + 'The function f has encountered an infinite loop. It has no base case.', + [theCode] + ) + .silentRun(); + }); + + test('shows infinite loop error if coinflip is true', () => { + state = { ...state, session: { ...state.session, experimentCoinflip: true } }; + const thisContext = createContext(3); + context = thisContext; + const theCode = 'function f(x){f(x);} f(1);'; + + return expectSaga(evalCode, theCode, context, execTime, workspaceLocation, actionType) + .withState(state) + .silentRun() + .then(result => { + const lastError = thisContext.errors[thisContext.errors.length - 1]; + expect(lastError.explain()).toContain('no base case'); + }); + }); + + test('does not show infinite loop error if coinflip is false', () => { + state = { ...state, session: { ...state.session, experimentCoinflip: false } }; + const thisContext = createContext(3); + context = thisContext; + const theCode = 'function f(x){f(x);} f(1);'; + + return expectSaga(evalCode, theCode, context, execTime, workspaceLocation, actionType) + .withState(state) + .silentRun() + .then(result => { + const lastError = thisContext.errors[thisContext.errors.length - 1]; + expect(lastError.explain()).not.toContain('no base case'); + }); + }); }); describe('on DEBUG_RESUME action without interruptions or pausing', () => { @@ -838,7 +907,8 @@ describe('evalTestCode', () => { value = 'another test value'; options = { scheduler: 'preemptive', - originalMaxExecTime: 1000 + originalMaxExecTime: 1000, + throwInfiniteLoops: true }; index = 1; type = TestcaseTypes.public; diff --git a/src/commons/utils/InfiniteLoopReporter.ts b/src/commons/utils/InfiniteLoopReporter.ts index 260729ffda..cfc36cab31 100644 --- a/src/commons/utils/InfiniteLoopReporter.ts +++ b/src/commons/utils/InfiniteLoopReporter.ts @@ -1,104 +1,5 @@ import * as Sentry from '@sentry/browser'; -import { infiniteLoopErrorType } from 'js-slang/dist/infiniteLoops/errorMessages'; -import { Context, SourceError } from 'js-slang/dist/types'; - -function getInfiniteLoopErrors(errors: SourceError[]) { - for (const error of errors) { - const errorType = infiniteLoopErrorType(error.explain()); - if (errorType) return errorType; - } - return ''; -} - -/** - * stringifies an array, similar to JSON.stringify, but is able to - * 'stringify' functions in arrays (e.g. [(x=>x)]) -> "[(x=>x)]" - * JSON.stringify will return [null]). Also written iteratively as - * stringifying long (lispy) lists are likely to cause stack - * overflows. Has a depth limit in case of circular references. - * - */ -function stringifyArray(arr: any[]) { - let result = ''; - const stack: [any[], integer][] = [[arr, 0]]; - while (stack.length > 0) { - // arbitrary depth limit: 1000 - if (stack.length > 1000) return result + ' ...REDACTED]'; - const top = stack.pop() as [any[], integer]; - const topArr = top[0]; - let idx = top[1]; - if (idx === 0) result += '['; - while (idx < topArr.length) { - const elem = topArr[idx]; - if (Array.isArray(elem)) { - stack.push([topArr, idx + 1]); - stack.push([elem, 0]); - break; - } else if (typeof elem === 'function') { - result += elem.toString(); - } else { - result += JSON.stringify(elem); - } - if (idx !== topArr.length - 1) result += ','; - idx++; - } - if (idx >= topArr.length) { - const top = stack[stack.length - 1]; - result += ']'; - if (top && top[1] !== top[0].length) result += ','; - } - } - return result; -} - -/** - * After running code in the REPL, the code from previous invocations/runs - * are stored as native javascript objects in the Context's NativeStorage object. - * This function turns these native objects back into string (transpiled code) form. - * NOTE: variables trapped in closures can not be saved/recovered, e.g. - * function f(x) {return (y)=>(x+y)}; //f(2).tostring() = "(y)=>(x+y)", 2 is gone forever - * - * @param {Globals} previousIdentifiers - Globals object from context.nativeStorage - * - * @returns {string} code - */ -function getPreviousCode(context: Context): string { - let code = ''; - for (const key of context.nativeStorage.previousProgramsIdentifiers) { - const theVar = context.nativeStorage.evaller!(key); - //add newline for readability - if (code !== '') code += '\n'; - if (typeof theVar === 'function') { - code += `${theVar.toString()}`; - } else if (Array.isArray(theVar)) { - code += `const ${key}=${stringifyArray(theVar)};`; - } else { - code += `const ${key}=${JSON.stringify(theVar)};`; - } - } - return code; -} - -/** - * Determines whether the error is an infinite loop, and returns a pair of [error type, code]. - * Constants (including functions) created from previous executions in the editor/REPL - * will be stored as native Javascript objects. We will convert these objects - * back into Source code and prepend it to the code that is being run. - * - * @param {Globals} scope - Globals object from context.nativeStorage - * @param {string} code - last block of code that was executed - * - * @returns {[string, string]} [error type, code] if the error was an infinite loop - * @returns {null} otherwise - */ -export function getInfiniteLoopData(context: Context, code: string) { - const errors = getInfiniteLoopErrors(context.errors); - if (errors) { - return [errors, getPreviousCode(context)]; - } else { - return null; - } -} +import { getInfiniteLoopData, InfiniteLoopErrorType } from 'js-slang/dist/infiniteLoops/errors'; /** * Sends the infinite loop data to Sentry. Uses a unique Sentry @@ -108,16 +9,25 @@ export function getInfiniteLoopData(context: Context, code: string) { * @param {string} errors - infinite loop error classification * @param {string} code - code to be sent along with the error */ -export function reportInfiniteLoopError(errors: string, code: string) { +function reportInfiniteLoopError( + errorType: InfiniteLoopErrorType, + isStream: boolean, + message: string, + code: string[] +) { Sentry.withScope(function (scope) { scope.clearBreadcrumbs(); scope.setLevel(Sentry.Severity.Info); - scope.setTag('code-type', errors); - scope.setExtra('code', code); - scope.setFingerprint(['INFINITE_LOOP_LOGGING_FINGERPRINT']); + scope.setTag('error-type', InfiniteLoopErrorType[errorType]); + scope.setTag('is-stream', isStream ? 'yes' : 'no'); + scope.setTag('message', message); + scope.setExtra('code', JSON.stringify(code)); + scope.setFingerprint(['INFINITE_LOOP_LOGGING_FINGERPRINT_2022']); const err = new Error('Infinite Loop'); // remove stack trace whenever we can to save space err.stack = ''; Sentry.captureException(err); }); } + +export { getInfiniteLoopData, reportInfiniteLoopError, InfiniteLoopErrorType }; From 0b4f3c5efbaa0942b891b053248bd69344918372 Mon Sep 17 00:00:00 2001 From: joeychensmart Date: Mon, 26 Jul 2021 16:25:03 +0800 Subject: [PATCH 2/2] Bump js-slang --- package.json | 2 +- yarn.lock | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a24607ec49..a5350e8fdc 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "connected-react-router": "^6.9.1", "flexboxgrid": "^6.3.1", "flexboxgrid-helpers": "^1.1.3", - "js-slang": "^0.5.10", + "js-slang": "^0.5.12", "konva": "^7.2.5", "lodash": "^4.17.21", "lz-string": "^1.4.4", diff --git a/yarn.lock b/yarn.lock index 04b05d438e..b468fdfce1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1529,6 +1529,11 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" +"@joeychenofficial/alt-ergo-modified@^2.4.0": + version "2.4.0" + resolved "https://registry.yarnpkg.com/@joeychenofficial/alt-ergo-modified/-/alt-ergo-modified-2.4.0.tgz#27aec0cbed8ab4e2f0dad6feb4f0c9766ac3132f" + integrity sha512-58b0K8pNUVZXGbua4IJQ+1K+E+jz3MkhDazZaaeKlD+sOLYR9iTHIbicV/I5K16ivYW6R9lONiT3dz8rMeFJ1w== + "@mapbox/node-pre-gyp@^1.0.0": version "1.0.5" resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.5.tgz#2a0b32fcb416fb3f2250fd24cb2a81421a4f5950" @@ -8181,11 +8186,12 @@ jest@26.6.0: import-local "^3.0.2" jest-cli "^26.6.0" -js-slang@^0.5.10: - version "0.5.10" - resolved "https://registry.yarnpkg.com/js-slang/-/js-slang-0.5.10.tgz#c65a3043fa87d5206c65c85d4c3773922cc6eba9" - integrity sha512-ToWrQ7E/UDOEFwgh3zst0VT9cuISYi4EijYynAcHwXRPu0XLQlZEtW4OjALX7eK+sfDoKeW6IWnJBDMAPVF8MQ== +js-slang@^0.5.12: + version "0.5.12" + resolved "https://registry.yarnpkg.com/js-slang/-/js-slang-0.5.12.tgz#63c77476bc8445dfc06691fb83c4cb6af683d072" + integrity sha512-P7246r6CO/mLr42q8pp9y5ILSs6WzF/2zyFAzhhIlUR5/jmS66nqk3Gw+qn/KSDbOhPWeimslJ1dc/azmzBo4w== dependencies: + "@joeychenofficial/alt-ergo-modified" "^2.4.0" "@types/estree" "0.0.47" acorn "^8.0.3" acorn-loose "^8.0.0"