Skip to content

Commit b4de94d

Browse files
committedMar 17, 2025
Refactor runners to include execution method tests
1 parent 95ccecd commit b4de94d

File tree

7 files changed

+626
-284
lines changed

7 files changed

+626
-284
lines changed
 

‎src/runner/__tests__/execMethod.ts

+334
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
import { JestAssertionError } from 'expect'
2+
import runners, { type RunnerTypes } from '../sourceRunner'
3+
import { Chapter, type ExecutionMethod, Variant } from '../../types'
4+
import type { Runner } from '../types'
5+
import { DEFAULT_SOURCE_OPTIONS, runCodeInSource } from '..'
6+
import { mockContext } from '../../utils/testing/mocks'
7+
import { getChapterName, objectKeys, objectValues } from '../../utils/misc'
8+
import { asMockedFunc } from '../../utils/testing/misc'
9+
import { parseError } from '../..'
10+
11+
jest.mock('../sourceRunner', () => ({
12+
default: new Proxy({} as Record<string, Runner>, {
13+
get: (obj, prop: string) => {
14+
if (!(prop in obj)) {
15+
const mockRunner: Runner = (_, context) =>
16+
Promise.resolve({
17+
status: 'finished',
18+
value: '',
19+
context
20+
})
21+
22+
obj[prop] = jest.fn(mockRunner)
23+
}
24+
return obj[prop]
25+
}
26+
})
27+
}))
28+
29+
// Required since Typed variant tries to load modules
30+
jest.mock('../../modules/loader')
31+
32+
beforeEach(() => {
33+
jest.clearAllMocks()
34+
})
35+
36+
interface TestCase {
37+
chapter?: Chapter
38+
variant?: Variant
39+
code?: string
40+
/**
41+
* Set this to simulate the options having
42+
* a specific execution method set
43+
*/
44+
optionMethod?: ExecutionMethod
45+
/**
46+
* Set this to simulate the context having a specific
47+
* execution method set
48+
*/
49+
contextMethod?: ExecutionMethod
50+
}
51+
52+
interface FullTestCase extends TestCase {
53+
/**
54+
* Which runner was expected to be called
55+
*/
56+
expectedRunner: RunnerTypes
57+
58+
/**
59+
* Should the runner have evaluated the prelude?
60+
*/
61+
expectedPrelude: boolean
62+
63+
verboseErrors?: boolean
64+
}
65+
66+
const sourceCases: FullTestCase[] = [
67+
{
68+
chapter: Chapter.SOURCE_1,
69+
variant: Variant.DEFAULT,
70+
expectedRunner: 'native',
71+
expectedPrelude: true
72+
},
73+
{
74+
chapter: Chapter.SOURCE_2,
75+
variant: Variant.DEFAULT,
76+
expectedRunner: 'native',
77+
expectedPrelude: true
78+
},
79+
{
80+
chapter: Chapter.SOURCE_3,
81+
variant: Variant.DEFAULT,
82+
expectedRunner: 'native',
83+
expectedPrelude: true
84+
},
85+
{
86+
chapter: Chapter.SOURCE_4,
87+
variant: Variant.DEFAULT,
88+
expectedRunner: 'native',
89+
expectedPrelude: true
90+
},
91+
{
92+
contextMethod: 'native',
93+
variant: Variant.NATIVE,
94+
expectedRunner: 'native',
95+
expectedPrelude: false
96+
}
97+
]
98+
99+
// These JS cases never evaluate a prelude,
100+
// nor ever have verbose errors enabled
101+
const fullJSCases: TestCase[] = [
102+
{ chapter: Chapter.FULL_JS },
103+
{ chapter: Chapter.FULL_TS }
104+
]
105+
106+
// The alt langs never evaluate a prelude,
107+
// always use fullJS regardless of variant,
108+
// but we don't need to check for verbose errors
109+
const altLangCases: Chapter[] = [Chapter.PYTHON_1]
110+
111+
type TestObject = {
112+
code: string
113+
chapter: Chapter
114+
variant: Variant
115+
expectedPrelude: boolean
116+
expectedRunner: RunnerTypes
117+
optionMethod?: ExecutionMethod
118+
contextMethod?: ExecutionMethod
119+
}
120+
121+
function expectCalls(count: number, expected: RunnerTypes) {
122+
const unexpectedRunner = objectKeys(runners).find(runner => {
123+
const { calls } = asMockedFunc(runners[runner]).mock
124+
return calls.length > 0
125+
})
126+
127+
switch (unexpectedRunner) {
128+
case undefined:
129+
throw new JestAssertionError(`Expected ${expected} to be called ${count} times, but no runners were called`)
130+
case expected: {
131+
expect(runners[expected]).toHaveBeenCalledTimes(count)
132+
return asMockedFunc(runners[expected]).mock.calls
133+
}
134+
default: {
135+
const callCount = asMockedFunc(runners[unexpectedRunner]).mock.calls.length
136+
throw new JestAssertionError(`Expected ${expected} to be called ${count} times, but ${unexpectedRunner} was called ${callCount} times`)
137+
}
138+
}
139+
}
140+
141+
async function testCase({
142+
code,
143+
chapter,
144+
variant,
145+
contextMethod,
146+
optionMethod,
147+
expectedPrelude,
148+
expectedRunner
149+
}: TestObject) {
150+
const context = mockContext(chapter, variant)
151+
if (contextMethod !== undefined) {
152+
context.executionMethod = contextMethod
153+
}
154+
155+
// Check if the prelude is null before execution
156+
// because the prelude gets set to null if it wasn't before
157+
const shouldPrelude = expectedPrelude && context.prelude !== null
158+
const options = { ...DEFAULT_SOURCE_OPTIONS }
159+
160+
if (optionMethod !== undefined) {
161+
options.executionMethod = optionMethod
162+
}
163+
164+
await runCodeInSource(code, context, options)
165+
166+
if (context.errors.length > 0) {
167+
console.log(parseError(context.errors))
168+
}
169+
170+
expect(context.errors.length).toEqual(0)
171+
172+
if (shouldPrelude) {
173+
// If the prelude was to be evaluated and the prelude is not null,
174+
// the runner should be called twice
175+
const [call0, call1] = expectCalls(2, expectedRunner)
176+
177+
// First with isPrelude true
178+
expect(call0[2].isPrelude).toEqual(true)
179+
180+
// and then with isPrelude false
181+
expect(call1[2].isPrelude).toEqual(false)
182+
} else {
183+
// If not, the runner should only have been called once
184+
const [call0] = expectCalls(1, expectedRunner)
185+
186+
// with isPrelude false
187+
expect(call0[2].isPrelude).toEqual(false)
188+
}
189+
}
190+
191+
function testCases(desc: string, cases: FullTestCase[]) {
192+
describe(desc, () =>
193+
test.each(
194+
cases.map(({ code, verboseErrors, contextMethod, chapter, variant, ...tc }, i) => {
195+
chapter = chapter ?? Chapter.SOURCE_1
196+
variant = variant ?? Variant.DEFAULT
197+
const context = mockContext(chapter, variant)
198+
if (contextMethod !== undefined) {
199+
context.executionMethod = contextMethod
200+
}
201+
202+
const chapterName = getChapterName(chapter)
203+
let desc = `${i + 1}. Testing ${chapterName}, Variant: ${variant}, expected ${tc.expectedRunner} runner`
204+
code = code ?? ''
205+
if (verboseErrors) {
206+
code = `"enable verbose";\n${code}`
207+
desc += ' (verbose errors)'
208+
}
209+
210+
return [desc, { code, chapter, variant, ...tc }]
211+
})
212+
)('%s', async (_, to) => testCase(to))
213+
)
214+
}
215+
216+
describe('Ensure that the correct runner is used for the given evaluation context and settings', () => {
217+
testCases('Test regular source cases', sourceCases)
218+
testCases(
219+
'Test source verbose error cases',
220+
sourceCases.map(
221+
tc => ({
222+
...tc,
223+
verboseErrors: true,
224+
expectedRunner: 'cse-machine'
225+
})
226+
)
227+
)
228+
229+
testCases(
230+
'Test source cases with debugger statements',
231+
sourceCases.map(
232+
tc => ({
233+
...tc,
234+
code: 'debugger;\n' + (tc.code ?? ''),
235+
expectedRunner: 'cse-machine'
236+
})
237+
)
238+
)
239+
240+
testCases('Test explicit control variant', sourceCases.map(tc => ({
241+
...tc,
242+
variant: Variant.EXPLICIT_CONTROL,
243+
expectedRunner: 'cse-machine'
244+
})))
245+
246+
testCases(
247+
'Test FullJS cases',
248+
fullJSCases.flatMap((tc): FullTestCase[] => {
249+
const fullCase: FullTestCase = {
250+
...tc,
251+
verboseErrors: false,
252+
expectedPrelude: false,
253+
expectedRunner: 'fulljs'
254+
}
255+
256+
const verboseErrorCase: FullTestCase = {
257+
...fullCase,
258+
verboseErrors: true
259+
}
260+
261+
return [fullCase, verboseErrorCase]
262+
})
263+
)
264+
265+
testCases(
266+
'Test alt-langs',
267+
altLangCases.flatMap(chapter =>
268+
objectValues(Variant).map(
269+
(variant): FullTestCase => ({
270+
code: '',
271+
variant,
272+
chapter,
273+
expectedPrelude: false,
274+
expectedRunner: 'fulljs',
275+
verboseErrors: false
276+
})
277+
)
278+
)
279+
)
280+
281+
test('if optionMethod is specified, verbose errors is ignored', () => testCase({
282+
code: '"enable verbose"; 0;',
283+
optionMethod: 'native',
284+
chapter: Chapter.SOURCE_4,
285+
variant: Variant.DEFAULT,
286+
expectedPrelude: true,
287+
expectedRunner: 'native'
288+
}))
289+
290+
test('if optionMethod is specified, debubger statements are ignored', () => testCase({
291+
code: 'debugger; 0;',
292+
optionMethod: 'native',
293+
chapter: Chapter.SOURCE_4,
294+
variant: Variant.DEFAULT,
295+
expectedPrelude: true,
296+
expectedRunner: 'native'
297+
}))
298+
299+
test('if contextMethod is specified, verbose errors is ignored', () => testCase({
300+
code: '"enable verbose"; 0;',
301+
contextMethod: 'native',
302+
chapter: Chapter.SOURCE_4,
303+
variant: Variant.DEFAULT,
304+
expectedPrelude: true,
305+
expectedRunner: 'native'
306+
}))
307+
308+
test('if contextMethod is specified, debugger statements are ignored', () => testCase({
309+
code: 'debugger; 0;',
310+
contextMethod: 'native',
311+
chapter: Chapter.SOURCE_4,
312+
variant: Variant.DEFAULT,
313+
expectedPrelude: true,
314+
expectedRunner: 'native'
315+
}))
316+
317+
test('optionMethod takes precedence over contextMethod', () => testCase({
318+
code: '0;',
319+
contextMethod: 'native',
320+
optionMethod: 'cse-machine',
321+
chapter: Chapter.SOURCE_4,
322+
variant: Variant.DEFAULT,
323+
expectedPrelude: true,
324+
expectedRunner: 'cse-machine'
325+
}))
326+
327+
test('debugger statements require cse-machine', () => testCase({
328+
code: 'debugger; 0;',
329+
chapter: Chapter.SOURCE_4,
330+
variant: Variant.DEFAULT,
331+
expectedPrelude: true,
332+
expectedRunner: 'cse-machine'
333+
}))
334+
})

‎src/runner/__tests__/modules.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { mockContext } from '../../utils/testing/mocks'
22
import { Chapter, Variant } from '../../types'
33
import { stripIndent } from '../../utils/formatters'
44
import { expectFinishedResultValue } from '../../utils/testing/misc'
5-
import { runCodeInSource } from '../sourceRunner'
5+
import { runCodeInSource } from '..'
6+
import { getChapterName } from '../../utils/misc'
67

78
jest.mock('../../modules/loader/loaders')
89

@@ -57,7 +58,7 @@ describe.each(describeCases)(
5758
'Ensuring that %s chapters are able to load modules',
5859
(_, chapters, variants, code) => {
5960
const chapterCases = chapters.map(chapterVal => {
60-
const [chapterName] = Object.entries(Chapter).find(([, value]) => value === chapterVal)!
61+
const chapterName = getChapterName(chapterVal)
6162
const index = chapters.indexOf(chapterVal)
6263
const variant = variants[index]
6364
return [`Testing ${chapterName}`, chapterVal, variant] as [string, Chapter, Variant]

‎src/runner/fullJSRunner.ts

+4-7
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ import { generate } from 'astring'
33
import type es from 'estree'
44
import { RawSourceMap } from 'source-map'
55

6-
import type { Result } from '..'
76
import { NATIVE_STORAGE_ID } from '../constants'
87
import { RuntimeSourceError } from '../errors/runtimeSourceError'
9-
import type { ImportOptions } from '../modules/moduleTypes'
108
import { parse } from '../parser/parser'
119
import {
1210
evallerReplacer,
@@ -19,6 +17,7 @@ import * as create from '../utils/ast/astCreator'
1917
import { getFunctionDeclarationNamesInProgram } from '../utils/uniqueIds'
2018
import { toSourceError } from './errors'
2119
import { resolvedErrorPromise } from './utils'
20+
import { Runner } from './types'
2221

2322
function fullJSEval(code: string, nativeStorage: NativeStorage): any {
2423
if (nativeStorage.evaller) {
@@ -46,11 +45,7 @@ function containsPrevEval(context: Context): boolean {
4645
return context.nativeStorage.evaller != null
4746
}
4847

49-
export async function fullJSRunner(
50-
program: es.Program,
51-
context: Context,
52-
importOptions: ImportOptions
53-
): Promise<Result> {
48+
const fullJSRunner: Runner = async (program, context) => {
5449
// prelude & builtins
5550
// only process builtins and preludes if it is a fresh eval context
5651
const prelude = preparePrelude(context)
@@ -91,3 +86,5 @@ export async function fullJSRunner(
9186
return resolvedErrorPromise
9287
}
9388
}
89+
90+
export default fullJSRunner

‎src/runner/index.ts

+185-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,185 @@
1-
export * from './fullJSRunner'
2-
export * from './htmlRunner'
3-
export * from './sourceRunner'
4-
export * from './utils'
1+
import * as _ from 'lodash'
2+
import type { Program } from 'estree'
3+
import type { Context, IOptions, Result } from '..'
4+
import { mapResult } from '../alt-langs/mapper'
5+
import type { FileGetter } from '../modules/moduleTypes'
6+
import preprocessFileImports from '../modules/preprocessor'
7+
import { Chapter, Variant, type RecursivePartial } from '../types'
8+
import { validateAndAnnotate } from '../validator/validator'
9+
import { parse } from '../parser/parser'
10+
import assert from '../utils/assert'
11+
import { defaultAnalysisOptions } from '../modules/preprocessor/analyzer'
12+
import { defaultLinkerOptions } from '../modules/preprocessor/linker'
13+
import { determineExecutionMethod, determineVariant, resolvedErrorPromise } from './utils'
14+
import runners from './sourceRunner'
15+
16+
let previousCode: {
17+
files: Partial<Record<string, string>>
18+
entrypointFilePath: string
19+
} | null = null
20+
21+
export const DEFAULT_SOURCE_OPTIONS: Readonly<IOptions> = {
22+
steps: 1000,
23+
stepLimit: -1,
24+
executionMethod: 'auto',
25+
variant: Variant.DEFAULT,
26+
originalMaxExecTime: 1000,
27+
useSubst: false,
28+
isPrelude: false,
29+
throwInfiniteLoops: true,
30+
envSteps: -1,
31+
importOptions: {
32+
...defaultAnalysisOptions,
33+
...defaultLinkerOptions,
34+
loadTabs: true
35+
},
36+
shouldAddFileName: null
37+
}
38+
39+
async function sourceRunner(
40+
program: Program,
41+
context: Context,
42+
isVerboseErrorsEnabled: boolean,
43+
options: RecursivePartial<IOptions> = {}
44+
): Promise<Result> {
45+
// It is necessary to make a copy of the DEFAULT_SOURCE_OPTIONS object because merge()
46+
// will modify it rather than create a new object
47+
const theOptions = _.merge({ ...DEFAULT_SOURCE_OPTIONS }, options)
48+
context.variant = determineVariant(context, options)
49+
50+
if (
51+
context.chapter === Chapter.FULL_JS ||
52+
context.chapter === Chapter.FULL_TS ||
53+
context.chapter === Chapter.PYTHON_1
54+
) {
55+
return runners.fulljs(program, context, theOptions)
56+
}
57+
58+
validateAndAnnotate(program, context)
59+
if (context.errors.length > 0) {
60+
return resolvedErrorPromise
61+
}
62+
63+
if (theOptions.useSubst) {
64+
return runners.substitution(program, context, theOptions)
65+
}
66+
67+
determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
68+
69+
// native, don't evaluate prelude
70+
if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
71+
return runners.fulljs(program, context, theOptions)
72+
}
73+
74+
// All runners after this point evaluate the prelude.
75+
if (context.prelude !== null) {
76+
context.unTypecheckedCode.push(context.prelude)
77+
const prelude = parse(context.prelude, context)
78+
if (prelude === null) {
79+
return resolvedErrorPromise
80+
}
81+
context.prelude = null
82+
await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
83+
return sourceRunner(program, context, isVerboseErrorsEnabled, options)
84+
}
85+
86+
if (context.variant === Variant.EXPLICIT_CONTROL || context.executionMethod === 'cse-machine') {
87+
if (options.isPrelude) {
88+
const preludeContext = { ...context, runtime: { ...context.runtime, debuggerOn: false } }
89+
const result = await runners['cse-machine'](program, preludeContext, theOptions)
90+
// Update object count in main program context after prelude is run
91+
context.runtime.objectCount = preludeContext.runtime.objectCount
92+
return result
93+
}
94+
return runners['cse-machine'](program, context, theOptions)
95+
}
96+
97+
assert(
98+
context.executionMethod !== 'auto',
99+
'Execution method should have been properly determined!'
100+
)
101+
return runners.native(program, context, theOptions)
102+
}
103+
104+
/**
105+
* Returns both the Result of the evaluated program, as well as
106+
* `verboseErrors`.
107+
*/
108+
export async function sourceFilesRunner(
109+
filesInput: FileGetter,
110+
entrypointFilePath: string,
111+
context: Context,
112+
options: RecursivePartial<IOptions> = {}
113+
): Promise<{
114+
result: Result
115+
verboseErrors: boolean
116+
}> {
117+
const preprocessResult = await preprocessFileImports(
118+
filesInput,
119+
entrypointFilePath,
120+
context,
121+
options
122+
)
123+
124+
if (!preprocessResult.ok) {
125+
return {
126+
result: { status: 'error' },
127+
verboseErrors: preprocessResult.verboseErrors
128+
}
129+
}
130+
131+
const { files, verboseErrors, program: preprocessedProgram } = preprocessResult
132+
133+
context.variant = determineVariant(context, options)
134+
// FIXME: The type checker does not support the typing of multiple files, so
135+
// we only push the code in the entrypoint file. Ideally, all files
136+
// involved in the program evaluation should be type-checked. Either way,
137+
// the type checker is currently not used at all so this is not very
138+
// urgent.
139+
context.unTypecheckedCode.push(files[entrypointFilePath])
140+
141+
const currentCode = {
142+
files,
143+
entrypointFilePath
144+
}
145+
context.shouldIncreaseEvaluationTimeout = _.isEqual(previousCode, currentCode)
146+
previousCode = currentCode
147+
148+
context.previousPrograms.unshift(preprocessedProgram)
149+
150+
const result = await sourceRunner(preprocessedProgram, context, verboseErrors, options)
151+
const resultMapper = mapResult(context)
152+
153+
return {
154+
result: resultMapper(result),
155+
verboseErrors
156+
}
157+
}
158+
159+
/**
160+
* Useful for just running a single line of code with the given context
161+
* However, if this single line of code is an import statement,
162+
* then the FileGetter is necessary, otherwise all local imports will
163+
* fail with ModuleNotFoundError
164+
*/
165+
export function runCodeInSource(
166+
code: string,
167+
context: Context,
168+
options: RecursivePartial<IOptions> = {},
169+
defaultFilePath: string = '/default.js',
170+
fileGetter?: FileGetter
171+
) {
172+
return sourceFilesRunner(
173+
path => {
174+
if (path === defaultFilePath) return Promise.resolve(code)
175+
if (!fileGetter) return Promise.resolve(undefined)
176+
return fileGetter(path)
177+
},
178+
defaultFilePath,
179+
context,
180+
options
181+
)
182+
}
183+
184+
export { htmlRunner } from './htmlRunner'
185+
export { resolvedErrorPromise }

‎src/runner/sourceRunner.ts

+91-271
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,13 @@
1-
import type es from 'estree'
21
import * as _ from 'lodash'
32
import type { RawSourceMap } from 'source-map'
43

5-
import { type IOptions, type Result } from '..'
64
import { JSSLANG_PROPERTIES } from '../constants'
75
import { CSEResultPromise, evaluate as CSEvaluate } from '../cse-machine/interpreter'
86
import { ExceptionError } from '../errors/errors'
97
import { RuntimeSourceError } from '../errors/runtimeSourceError'
108
import { TimeoutError } from '../errors/timeoutErrors'
119
import { isPotentialInfiniteLoop } from '../infiniteLoops/errors'
1210
import { testForInfiniteLoop } from '../infiniteLoops/runtime'
13-
import preprocessFileImports from '../modules/preprocessor'
14-
import { defaultAnalysisOptions } from '../modules/preprocessor/analyzer'
15-
import { defaultLinkerOptions } from '../modules/preprocessor/linker'
16-
import { parse } from '../parser/parser'
1711
import {
1812
callee,
1913
getEvaluationSteps,
@@ -23,288 +17,114 @@ import {
2317
} from '../stepper/stepper'
2418
import { sandboxedEval } from '../transpiler/evalContainer'
2519
import { transpile } from '../transpiler/transpiler'
26-
import { Chapter, type Context, type RecursivePartial, Variant } from '../types'
27-
import { validateAndAnnotate } from '../validator/validator'
28-
import type { FileGetter } from '../modules/moduleTypes'
29-
import { mapResult } from '../alt-langs/mapper'
30-
import assert from '../utils/assert'
20+
import { Variant } from '../types'
3121
import { toSourceError } from './errors'
32-
import { fullJSRunner } from './fullJSRunner'
33-
import { determineExecutionMethod, determineVariant, resolvedErrorPromise } from './utils'
22+
import { resolvedErrorPromise } from './utils'
23+
import type { Runner } from './types'
24+
import fullJSRunner from './fullJSRunner'
3425

35-
export const DEFAULT_SOURCE_OPTIONS: Readonly<IOptions> = {
36-
steps: 1000,
37-
stepLimit: -1,
38-
executionMethod: 'auto',
39-
variant: Variant.DEFAULT,
40-
originalMaxExecTime: 1000,
41-
useSubst: false,
42-
isPrelude: false,
43-
throwInfiniteLoops: true,
44-
envSteps: -1,
45-
importOptions: {
46-
...defaultAnalysisOptions,
47-
...defaultLinkerOptions,
48-
loadTabs: true
49-
},
50-
shouldAddFileName: null
51-
}
52-
53-
let previousCode: {
54-
files: Partial<Record<string, string>>
55-
entrypointFilePath: string
56-
} | null = null
5726
let isPreviousCodeTimeoutError = false
58-
59-
function runSubstitution(
60-
program: es.Program,
61-
context: Context,
62-
options: IOptions
63-
): Promise<Result> {
64-
const steps = getEvaluationSteps(program, context, options)
65-
if (context.errors.length > 0) {
66-
return resolvedErrorPromise
67-
}
68-
const redexedSteps: IStepperPropContents[] = []
69-
for (const step of steps) {
70-
const redex = getRedex(step[0], step[1])
71-
const redexed = redexify(step[0], step[1])
72-
redexedSteps.push({
73-
code: redexed[0],
74-
redex: redexed[1],
75-
explanation: step[2],
76-
function: callee(redex, context)
77-
})
78-
}
79-
return Promise.resolve({
80-
status: 'finished',
81-
context,
82-
value: redexedSteps
83-
})
84-
}
85-
86-
async function runNative(
87-
program: es.Program,
88-
context: Context,
89-
options: IOptions
90-
): Promise<Result> {
91-
if (!options.isPrelude) {
92-
if (context.shouldIncreaseEvaluationTimeout && isPreviousCodeTimeoutError) {
93-
context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
94-
} else {
95-
context.nativeStorage.maxExecTime = options.originalMaxExecTime
27+
const runners = {
28+
fulljs: fullJSRunner,
29+
'cse-machine': (program, context, options) => {
30+
const value = CSEvaluate(program, context, options)
31+
return CSEResultPromise(context, value)
32+
},
33+
substitution: (program, context, options) => {
34+
const steps = getEvaluationSteps(program, context, options)
35+
if (context.errors.length > 0) {
36+
return resolvedErrorPromise
9637
}
97-
}
98-
99-
// For whatever reason, the transpiler mutates the state of the AST as it is transpiling and inserts
100-
// a bunch of global identifiers to it. Once that happens, the infinite loop detection instrumentation
101-
// ends up generating code that has syntax errors. As such, we need to make a deep copy here to preserve
102-
// the original AST for future use, such as with the infinite loop detector.
103-
const transpiledProgram = _.cloneDeep(program)
104-
let transpiled
105-
let sourceMapJson: RawSourceMap | undefined
106-
try {
107-
;({ transpiled, sourceMapJson } = transpile(transpiledProgram, context))
108-
let value = sandboxedEval(transpiled, context.nativeStorage)
109-
110-
if (!options.isPrelude) {
111-
isPreviousCodeTimeoutError = false
38+
const redexedSteps: IStepperPropContents[] = []
39+
for (const step of steps) {
40+
const redex = getRedex(step[0], step[1])
41+
const redexed = redexify(step[0], step[1])
42+
redexedSteps.push({
43+
code: redexed[0],
44+
redex: redexed[1],
45+
explanation: step[2],
46+
function: callee(redex, context)
47+
})
11248
}
113-
114-
return {
49+
return Promise.resolve({
11550
status: 'finished',
11651
context,
117-
value
52+
value: redexedSteps
53+
})
54+
},
55+
native: async (program, context, options) => {
56+
if (!options.isPrelude) {
57+
if (context.shouldIncreaseEvaluationTimeout && isPreviousCodeTimeoutError) {
58+
context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
59+
} else {
60+
context.nativeStorage.maxExecTime = options.originalMaxExecTime
61+
}
11862
}
119-
} catch (error) {
120-
const isDefaultVariant = options.variant === undefined || options.variant === Variant.DEFAULT
121-
if (isDefaultVariant && isPotentialInfiniteLoop(error)) {
122-
const detectedInfiniteLoop = testForInfiniteLoop(
123-
program,
124-
context.previousPrograms.slice(1),
125-
context.nativeStorage.loadedModules
126-
)
127-
if (detectedInfiniteLoop !== undefined) {
128-
if (options.throwInfiniteLoops) {
129-
context.errors.push(detectedInfiniteLoop)
130-
return resolvedErrorPromise
131-
} else {
132-
error.infiniteLoopError = detectedInfiniteLoop
133-
if (error instanceof ExceptionError) {
134-
;(error.error as any).infiniteLoopError = detectedInfiniteLoop
63+
64+
// For whatever reason, the transpiler mutates the state of the AST as it is transpiling and inserts
65+
// a bunch of global identifiers to it. Once that happens, the infinite loop detection instrumentation
66+
// ends up generating code that has syntax errors. As such, we need to make a deep copy here to preserve
67+
// the original AST for future use, such as with the infinite loop detector.
68+
const transpiledProgram = _.cloneDeep(program)
69+
let transpiled
70+
let sourceMapJson: RawSourceMap | undefined
71+
try {
72+
;({ transpiled, sourceMapJson } = transpile(transpiledProgram, context))
73+
let value = sandboxedEval(transpiled, context.nativeStorage)
74+
75+
if (!options.isPrelude) {
76+
isPreviousCodeTimeoutError = false
77+
}
78+
79+
return {
80+
status: 'finished',
81+
context,
82+
value
83+
}
84+
} catch (error) {
85+
const isDefaultVariant = options.variant === undefined || options.variant === Variant.DEFAULT
86+
if (isDefaultVariant && isPotentialInfiniteLoop(error)) {
87+
const detectedInfiniteLoop = testForInfiniteLoop(
88+
program,
89+
context.previousPrograms.slice(1),
90+
context.nativeStorage.loadedModules
91+
)
92+
if (detectedInfiniteLoop !== undefined) {
93+
if (options.throwInfiniteLoops) {
94+
context.errors.push(detectedInfiniteLoop)
95+
return resolvedErrorPromise
96+
} else {
97+
error.infiniteLoopError = detectedInfiniteLoop
98+
if (error instanceof ExceptionError) {
99+
;(error.error as any).infiniteLoopError = detectedInfiniteLoop
100+
}
135101
}
136102
}
137103
}
138-
}
139-
if (error instanceof RuntimeSourceError) {
140-
context.errors.push(error)
141-
if (error instanceof TimeoutError) {
142-
isPreviousCodeTimeoutError = true
143-
}
144-
return resolvedErrorPromise
145-
}
146-
if (error instanceof ExceptionError) {
147-
// if we know the location of the error, just throw it
148-
if (error.location.start.line !== -1) {
104+
if (error instanceof RuntimeSourceError) {
149105
context.errors.push(error)
106+
if (error instanceof TimeoutError) {
107+
isPreviousCodeTimeoutError = true
108+
}
150109
return resolvedErrorPromise
151-
} else {
152-
error = error.error // else we try to get the location from source map
153110
}
154-
}
155-
156-
const sourceError = await toSourceError(error, sourceMapJson)
157-
context.errors.push(sourceError)
158-
return resolvedErrorPromise
159-
}
160-
}
161-
162-
function runCSEMachine(program: es.Program, context: Context, options: IOptions): Promise<Result> {
163-
const value = CSEvaluate(program, context, options)
164-
return CSEResultPromise(context, value)
165-
}
166-
167-
async function sourceRunner(
168-
program: es.Program,
169-
context: Context,
170-
isVerboseErrorsEnabled: boolean,
171-
options: RecursivePartial<IOptions> = {}
172-
): Promise<Result> {
173-
// It is necessary to make a copy of the DEFAULT_SOURCE_OPTIONS object because merge()
174-
// will modify it rather than create a new object
175-
const theOptions = _.merge({ ...DEFAULT_SOURCE_OPTIONS }, options)
176-
context.variant = determineVariant(context, options)
177-
178-
if (
179-
context.chapter === Chapter.FULL_JS ||
180-
context.chapter === Chapter.FULL_TS ||
181-
context.chapter === Chapter.PYTHON_1
182-
) {
183-
return fullJSRunner(program, context, theOptions.importOptions)
184-
}
185-
186-
validateAndAnnotate(program, context)
187-
if (context.errors.length > 0) {
188-
return resolvedErrorPromise
189-
}
190-
191-
if (theOptions.useSubst) {
192-
return runSubstitution(program, context, theOptions)
193-
}
194-
195-
determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
196-
197-
// native, don't evaluate prelude
198-
if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
199-
return fullJSRunner(program, context, theOptions.importOptions)
200-
}
111+
if (error instanceof ExceptionError) {
112+
// if we know the location of the error, just throw it
113+
if (error.location.start.line !== -1) {
114+
context.errors.push(error)
115+
return resolvedErrorPromise
116+
} else {
117+
error = error.error // else we try to get the location from source map
118+
}
119+
}
201120

202-
// All runners after this point evaluate the prelude.
203-
if (context.prelude !== null) {
204-
context.unTypecheckedCode.push(context.prelude)
205-
const prelude = parse(context.prelude, context)
206-
if (prelude === null) {
121+
const sourceError = await toSourceError(error, sourceMapJson)
122+
context.errors.push(sourceError)
207123
return resolvedErrorPromise
208124
}
209-
context.prelude = null
210-
await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
211-
return sourceRunner(program, context, isVerboseErrorsEnabled, options)
212-
}
213-
214-
if (context.variant === Variant.EXPLICIT_CONTROL || context.executionMethod === 'cse-machine') {
215-
if (options.isPrelude) {
216-
const preludeContext = { ...context, runtime: { ...context.runtime, debuggerOn: false } }
217-
const result = await runCSEMachine(program, preludeContext, theOptions)
218-
// Update object count in main program context after prelude is run
219-
context.runtime.objectCount = preludeContext.runtime.objectCount
220-
return result
221-
}
222-
return runCSEMachine(program, context, theOptions)
223-
}
224-
225-
assert(
226-
context.executionMethod !== 'auto',
227-
'Execution method should have been properly determined!'
228-
)
229-
return runNative(program, context, theOptions)
230-
}
231-
232-
/**
233-
* Returns both the Result of the evaluated program, as well as
234-
* `verboseErrors`.
235-
*/
236-
export async function sourceFilesRunner(
237-
filesInput: FileGetter,
238-
entrypointFilePath: string,
239-
context: Context,
240-
options: RecursivePartial<IOptions> = {}
241-
): Promise<{
242-
result: Result
243-
verboseErrors: boolean
244-
}> {
245-
const preprocessResult = await preprocessFileImports(
246-
filesInput,
247-
entrypointFilePath,
248-
context,
249-
options
250-
)
251-
252-
if (!preprocessResult.ok) {
253-
return {
254-
result: { status: 'error' },
255-
verboseErrors: preprocessResult.verboseErrors
256-
}
257-
}
258-
259-
const { files, verboseErrors, program: preprocessedProgram } = preprocessResult
260-
261-
context.variant = determineVariant(context, options)
262-
// FIXME: The type checker does not support the typing of multiple files, so
263-
// we only push the code in the entrypoint file. Ideally, all files
264-
// involved in the program evaluation should be type-checked. Either way,
265-
// the type checker is currently not used at all so this is not very
266-
// urgent.
267-
context.unTypecheckedCode.push(files[entrypointFilePath])
268-
269-
const currentCode = {
270-
files,
271-
entrypointFilePath
272125
}
273-
context.shouldIncreaseEvaluationTimeout = _.isEqual(previousCode, currentCode)
274-
previousCode = currentCode
275-
276-
context.previousPrograms.unshift(preprocessedProgram)
126+
} satisfies Record<string, Runner>
277127

278-
const result = await sourceRunner(preprocessedProgram, context, verboseErrors, options)
279-
const resultMapper = mapResult(context)
280-
281-
return {
282-
result: resultMapper(result),
283-
verboseErrors
284-
}
285-
}
128+
export default runners
286129

287-
/**
288-
* Useful for just running a single line of code with the given context
289-
* However, if this single line of code is an import statement,
290-
* then the FileGetter is necessary, otherwise all local imports will
291-
* fail with ModuleNotFoundError
292-
*/
293-
export function runCodeInSource(
294-
code: string,
295-
context: Context,
296-
options: RecursivePartial<IOptions> = {},
297-
defaultFilePath: string = '/default.js',
298-
fileGetter?: FileGetter
299-
) {
300-
return sourceFilesRunner(
301-
path => {
302-
if (path === defaultFilePath) return Promise.resolve(code)
303-
if (!fileGetter) return Promise.resolve(undefined)
304-
return fileGetter(path)
305-
},
306-
defaultFilePath,
307-
context,
308-
options
309-
)
310-
}
130+
export type RunnerTypes = keyof typeof runners

‎src/runner/types.ts

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import type { Program } from 'estree'
2+
import type { Context, IOptions, Result } from '..'
3+
4+
export type Runner = (program: Program, context: Context, options: IOptions) => Promise<Result>

‎src/utils/misc.ts

+5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { RuntimeSourceError } from '../errors/runtimeSourceError'
2+
import { Chapter } from '../types'
23

34
export class PromiseTimeoutError extends RuntimeSourceError {}
45

@@ -42,3 +43,7 @@ export function objectKeys<T extends string | number | symbol>(obj: Record<T, an
4243
export function objectValues<T>(obj: Record<any, T>) {
4344
return Object.values(obj) as T[]
4445
}
46+
47+
export function getChapterName(chapter: Chapter) {
48+
return objectKeys(Chapter).find(name => Chapter[name] === chapter)!
49+
}

0 commit comments

Comments
 (0)
Please sign in to comment.