• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

source-academy / js-slang / 13576250348

27 Feb 2025 09:30PM UTC coverage: 81.255% (-0.3%) from 81.596%
13576250348

Pull #1725

github

web-flow
Merge 7a63ac3b0 into 1ec766986
Pull Request #1725: Remove Non-Det Interpreter

3571 of 4770 branches covered (74.86%)

Branch coverage included in aggregate %.

22 of 32 new or added lines in 7 files covered. (68.75%)

7 existing lines in 4 files now uncovered.

11154 of 13352 relevant lines covered (83.54%)

141402.47 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.24
/src/runner/sourceRunner.ts
1
import type es from 'estree'
2
import * as _ from 'lodash'
58✔
3
import type { RawSourceMap } from 'source-map'
4

5
import { type IOptions, type Result } from '..'
6
import { JSSLANG_PROPERTIES, UNKNOWN_LOCATION } from '../constants'
58✔
7
import { CSEResultPromise, evaluate as CSEvaluate } from '../cse-machine/interpreter'
58✔
8
import { ExceptionError } from '../errors/errors'
58✔
9
import { RuntimeSourceError } from '../errors/runtimeSourceError'
58✔
10
import { TimeoutError } from '../errors/timeoutErrors'
58✔
11
import { transpileToGPU } from '../gpu/gpu'
58✔
12
import { isPotentialInfiniteLoop } from '../infiniteLoops/errors'
58✔
13
import { testForInfiniteLoop } from '../infiniteLoops/runtime'
58✔
14
import { evaluateProgram as evaluate } from '../interpreter/interpreter'
58✔
15
import { transpileToLazy } from '../lazy/lazy'
58✔
16
import preprocessFileImports from '../modules/preprocessor'
58✔
17
import { defaultAnalysisOptions } from '../modules/preprocessor/analyzer'
58✔
18
import { defaultLinkerOptions } from '../modules/preprocessor/linker'
58✔
19
import { parse } from '../parser/parser'
58✔
20
import { AsyncScheduler, PreemptiveScheduler } from '../schedulers'
58✔
21
import {
58✔
22
  callee,
23
  getEvaluationSteps,
24
  getRedex,
25
  type IStepperPropContents,
26
  redexify
27
} from '../stepper/stepper'
28
import { sandboxedEval } from '../transpiler/evalContainer'
58✔
29
import { transpile } from '../transpiler/transpiler'
58✔
30
import { Chapter, type Context, type RecursivePartial, type Scheduler, Variant } from '../types'
58✔
31
import { forceIt } from '../utils/operators'
58✔
32
import { validateAndAnnotate } from '../validator/validator'
58✔
33
import { compileForConcurrent } from '../vm/svml-compiler'
58✔
34
import { runWithProgram } from '../vm/svml-machine'
58✔
35
import type { FileGetter } from '../modules/moduleTypes'
36
import { mapResult } from '../alt-langs/mapper'
58✔
37
import { toSourceError } from './errors'
58✔
38
import { fullJSRunner } from './fullJSRunner'
58✔
39
import { determineExecutionMethod, determineVariant, resolvedErrorPromise } from './utils'
58✔
40

41
const DEFAULT_SOURCE_OPTIONS: Readonly<IOptions> = {
58✔
42
  scheduler: 'async',
43
  steps: 1000,
44
  stepLimit: -1,
45
  executionMethod: 'auto',
46
  variant: Variant.DEFAULT,
47
  originalMaxExecTime: 1000,
48
  useSubst: false,
49
  isPrelude: false,
50
  throwInfiniteLoops: true,
51
  envSteps: -1,
52
  importOptions: {
53
    ...defaultAnalysisOptions,
54
    ...defaultLinkerOptions,
55
    loadTabs: true
56
  },
57
  shouldAddFileName: null
58
}
59

60
let previousCode: {
61
  files: Partial<Record<string, string>>
62
  entrypointFilePath: string
63
} | null = null
58✔
64
let isPreviousCodeTimeoutError = false
58✔
65

66
function runConcurrent(program: es.Program, context: Context, options: IOptions): Promise<Result> {
67
  if (context.shouldIncreaseEvaluationTimeout) {
103!
68
    context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
×
69
  } else {
70
    context.nativeStorage.maxExecTime = options.originalMaxExecTime
103✔
71
  }
72

73
  try {
103✔
74
    return Promise.resolve({
103✔
75
      status: 'finished',
76
      context,
77
      value: runWithProgram(compileForConcurrent(program, context), context)
78
    })
79
  } catch (error) {
80
    if (error instanceof RuntimeSourceError || error instanceof ExceptionError) {
31✔
81
      context.errors.push(error) // use ExceptionErrors for non Source Errors
2✔
82
      return resolvedErrorPromise
2✔
83
    }
84
    context.errors.push(new ExceptionError(error, UNKNOWN_LOCATION))
29✔
85
    return resolvedErrorPromise
29✔
86
  }
87
}
88

89
function runSubstitution(
90
  program: es.Program,
91
  context: Context,
92
  options: IOptions
93
): Promise<Result> {
94
  const steps = getEvaluationSteps(program, context, options)
×
95
  if (context.errors.length > 0) {
×
96
    return resolvedErrorPromise
×
97
  }
98
  const redexedSteps: IStepperPropContents[] = []
×
99
  for (const step of steps) {
×
100
    const redex = getRedex(step[0], step[1])
×
101
    const redexed = redexify(step[0], step[1])
×
102
    redexedSteps.push({
×
103
      code: redexed[0],
104
      redex: redexed[1],
105
      explanation: step[2],
106
      function: callee(redex, context)
107
    })
108
  }
109
  return Promise.resolve({
×
110
    status: 'finished',
111
    context,
112
    value: redexedSteps
113
  })
114
}
115

116
function runInterpreter(program: es.Program, context: Context, options: IOptions): Promise<Result> {
117
  let it = evaluate(program, context)
904✔
118
  let scheduler: Scheduler
119
  if (options.scheduler === 'async') {
904!
UNCOV
120
    scheduler = new AsyncScheduler()
×
121
  } else {
122
    scheduler = new PreemptiveScheduler(options.steps)
904✔
123
  }
124
  return scheduler.run(it, context)
904✔
125
}
126

127
async function runNative(
128
  program: es.Program,
129
  context: Context,
130
  options: IOptions
131
): Promise<Result> {
132
  if (!options.isPrelude) {
766✔
133
    if (context.shouldIncreaseEvaluationTimeout && isPreviousCodeTimeoutError) {
449✔
134
      context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
3✔
135
    } else {
136
      context.nativeStorage.maxExecTime = options.originalMaxExecTime
446✔
137
    }
138
  }
139

140
  // For whatever reason, the transpiler mutates the state of the AST as it is transpiling and inserts
141
  // a bunch of global identifiers to it. Once that happens, the infinite loop detection instrumentation
142
  // ends up generating code that has syntax errors. As such, we need to make a deep copy here to preserve
143
  // the original AST for future use, such as with the infinite loop detector.
144
  const transpiledProgram = _.cloneDeep(program)
766✔
145
  let transpiled
146
  let sourceMapJson: RawSourceMap | undefined
147
  try {
766✔
148
    switch (context.variant) {
766!
149
      case Variant.GPU:
150
        transpileToGPU(transpiledProgram)
×
151
        break
×
152
      case Variant.LAZY:
153
        transpileToLazy(transpiledProgram)
97✔
154
        break
97✔
155
    }
156

157
    ;({ transpiled, sourceMapJson } = transpile(transpiledProgram, context))
766✔
158
    let value = sandboxedEval(transpiled, context.nativeStorage)
764✔
159

160
    if (context.variant === Variant.LAZY) {
696✔
161
      value = forceIt(value)
89✔
162
    }
163

164
    if (!options.isPrelude) {
696✔
165
      isPreviousCodeTimeoutError = false
379✔
166
    }
167

168
    return {
696✔
169
      status: 'finished',
170
      context,
171
      value
172
    }
173
  } catch (error) {
174
    const isDefaultVariant = options.variant === undefined || options.variant === Variant.DEFAULT
70✔
175
    if (isDefaultVariant && isPotentialInfiniteLoop(error)) {
70✔
176
      const detectedInfiniteLoop = testForInfiniteLoop(
10✔
177
        program,
178
        context.previousPrograms.slice(1),
179
        context.nativeStorage.loadedModules
180
      )
181
      if (detectedInfiniteLoop !== undefined) {
10✔
182
        if (options.throwInfiniteLoops) {
10✔
183
          context.errors.push(detectedInfiniteLoop)
2✔
184
          return resolvedErrorPromise
2✔
185
        } else {
186
          error.infiniteLoopError = detectedInfiniteLoop
8✔
187
          if (error instanceof ExceptionError) {
8✔
188
            ;(error.error as any).infiniteLoopError = detectedInfiniteLoop
1✔
189
          }
190
        }
191
      }
192
    }
193
    if (error instanceof RuntimeSourceError) {
68✔
194
      context.errors.push(error)
44✔
195
      if (error instanceof TimeoutError) {
44✔
196
        isPreviousCodeTimeoutError = true
7✔
197
      }
198
      return resolvedErrorPromise
44✔
199
    }
200
    if (error instanceof ExceptionError) {
24✔
201
      // if we know the location of the error, just throw it
202
      if (error.location.start.line !== -1) {
24✔
203
        context.errors.push(error)
23✔
204
        return resolvedErrorPromise
23✔
205
      } else {
206
        error = error.error // else we try to get the location from source map
1✔
207
      }
208
    }
209

210
    const sourceError = await toSourceError(error, sourceMapJson)
1✔
211
    context.errors.push(sourceError)
1✔
212
    return resolvedErrorPromise
1✔
213
  }
214
}
215

216
function runCSEMachine(program: es.Program, context: Context, options: IOptions): Promise<Result> {
217
  const value = CSEvaluate(program, context, options)
1,034✔
218
  return CSEResultPromise(context, value)
1,034✔
219
}
220

221
async function sourceRunner(
222
  program: es.Program,
223
  context: Context,
224
  isVerboseErrorsEnabled: boolean,
225
  options: RecursivePartial<IOptions> = {}
×
226
): Promise<Result> {
227
  // It is necessary to make a copy of the DEFAULT_SOURCE_OPTIONS object because merge()
228
  // will modify it rather than create a new object
229
  const theOptions = _.merge({ ...DEFAULT_SOURCE_OPTIONS }, options)
4,047✔
230
  context.variant = determineVariant(context, options)
4,047✔
231

232
  if (
4,047✔
233
    context.chapter === Chapter.FULL_JS ||
12,114✔
234
    context.chapter === Chapter.FULL_TS ||
235
    context.chapter === Chapter.PYTHON_1
236
  ) {
237
    return fullJSRunner(program, context, theOptions.importOptions)
69✔
238
  }
239

240
  validateAndAnnotate(program, context)
3,978✔
241
  if (context.errors.length > 0) {
3,978✔
242
    return resolvedErrorPromise
7✔
243
  }
244

245
  if (context.variant === Variant.CONCURRENT) {
3,971✔
246
    return runConcurrent(program, context, theOptions)
103✔
247
  }
248

249
  if (theOptions.useSubst) {
3,868!
250
    return runSubstitution(program, context, theOptions)
×
251
  }
252

253
  determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
3,868✔
254

255
  // native, don't evaluate prelude
256
  if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
3,868✔
257
    return await fullJSRunner(program, context, theOptions.importOptions)
5✔
258
  }
259

260
  // All runners after this point evaluate the prelude.
261
  if (context.prelude !== null) {
3,863✔
262
    context.unTypecheckedCode.push(context.prelude)
1,159✔
263
    const prelude = parse(context.prelude, context)
1,159✔
264
    if (prelude === null) {
1,159!
265
      return resolvedErrorPromise
×
266
    }
267
    context.prelude = null
1,159✔
268
    await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
1,159✔
269
    return sourceRunner(program, context, isVerboseErrorsEnabled, options)
1,159✔
270
  }
271

272
  if (context.variant === Variant.EXPLICIT_CONTROL || context.executionMethod === 'cse-machine') {
2,704✔
273
    if (options.isPrelude) {
1,034✔
274
      const preludeContext = { ...context, runtime: { ...context.runtime, debuggerOn: false } }
473✔
275
      const result = await runCSEMachine(program, preludeContext, theOptions)
473✔
276
      // Update object count in main program context after prelude is run
277
      context.runtime.objectCount = preludeContext.runtime.objectCount
473✔
278
      return result
473✔
279
    }
280
    return runCSEMachine(program, context, theOptions)
561✔
281
  }
282

283
  if (context.executionMethod === 'native') {
1,670✔
284
    return runNative(program, context, theOptions)
766✔
285
  }
286

287
  return runInterpreter(program, context, theOptions)
904✔
288
}
289

290
/**
291
 * Returns both the Result of the evaluated program, as well as
292
 * `verboseErrors`.
293
 */
294
export async function sourceFilesRunner(
58✔
295
  filesInput: FileGetter,
296
  entrypointFilePath: string,
297
  context: Context,
298
  options: RecursivePartial<IOptions> = {}
×
299
): Promise<{
300
  result: Result
301
  verboseErrors: boolean
302
}> {
303
  const preprocessResult = await preprocessFileImports(
1,941✔
304
    filesInput,
305
    entrypointFilePath,
306
    context,
307
    options
308
  )
309

310
  if (!preprocessResult.ok) {
1,941✔
311
    return {
212✔
312
      result: { status: 'error' },
313
      verboseErrors: preprocessResult.verboseErrors
314
    }
315
  }
316

317
  const { files, verboseErrors, program: preprocessedProgram } = preprocessResult
1,729✔
318

319
  context.variant = determineVariant(context, options)
1,729✔
320
  // FIXME: The type checker does not support the typing of multiple files, so
321
  //        we only push the code in the entrypoint file. Ideally, all files
322
  //        involved in the program evaluation should be type-checked. Either way,
323
  //        the type checker is currently not used at all so this is not very
324
  //        urgent.
325
  context.unTypecheckedCode.push(files[entrypointFilePath])
1,729✔
326

327
  const currentCode = {
1,729✔
328
    files,
329
    entrypointFilePath
330
  }
331
  context.shouldIncreaseEvaluationTimeout = _.isEqual(previousCode, currentCode)
1,729✔
332
  previousCode = currentCode
1,729✔
333

334
  context.previousPrograms.unshift(preprocessedProgram)
1,729✔
335

336
  const result = await sourceRunner(preprocessedProgram, context, verboseErrors, options)
1,729✔
337
  const resultMapper = mapResult(context)
1,729✔
338

339
  return {
1,729✔
340
    result: resultMapper(result),
341
    verboseErrors
342
  }
343
}
344

345
/**
346
 * Useful for just running a single line of code with the given context
347
 * However, if this single line of code is an import statement,
348
 * then the FileGetter is necessary, otherwise all local imports will
349
 * fail with ModuleNotFoundError
350
 */
351
export function runCodeInSource(
58✔
352
  code: string,
353
  context: Context,
354
  options: RecursivePartial<IOptions> = {},
13✔
355
  defaultFilePath: string = '/default.js',
362✔
356
  fileGetter?: FileGetter
357
) {
358
  return sourceFilesRunner(
384✔
359
    path => {
360
      if (path === defaultFilePath) return Promise.resolve(code)
385✔
361
      if (!fileGetter) return Promise.resolve(undefined)
1!
362
      return fileGetter(path)
1✔
363
    },
364
    defaultFilePath,
365
    context,
366
    options
367
  )
368
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc