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

source-academy / js-slang / 5936042370

22 Aug 2023 07:58AM UTC coverage: 83.014% (+0.1%) from 82.893%
5936042370

Pull #1471

github

web-flow
Merge 6d88e2972 into 1d991166b
Pull Request #1471: Use Asynchronous Code for Loading Modules

3618 of 4779 branches covered (75.71%)

Branch coverage included in aggregate %.

269 of 269 new or added lines in 23 files covered. (100.0%)

10838 of 12635 relevant lines covered (85.78%)

106101.79 hits per line

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

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

5
import type { IOptions, Result } from '..'
6
import { JSSLANG_PROPERTIES, UNKNOWN_LOCATION } from '../constants'
67✔
7
import { ECEResultPromise, evaluate as ECEvaluate } from '../ec-evaluator/interpreter'
67✔
8
import { ExceptionError } from '../errors/errors'
67✔
9
import { CannotFindModuleError } from '../errors/localImportErrors'
67✔
10
import { RuntimeSourceError } from '../errors/runtimeSourceError'
67✔
11
import { TimeoutError } from '../errors/timeoutErrors'
67✔
12
import { transpileToGPU } from '../gpu/gpu'
67✔
13
import { isPotentialInfiniteLoop } from '../infiniteLoops/errors'
67✔
14
import { testForInfiniteLoop } from '../infiniteLoops/runtime'
67✔
15
import { evaluateProgram as evaluate } from '../interpreter/interpreter'
67✔
16
import { nonDetEvaluate } from '../interpreter/interpreter-non-det'
67✔
17
import { transpileToLazy } from '../lazy/lazy'
67✔
18
import preprocessFileImports from '../localImports/preprocessor'
67✔
19
import { getRequireProvider } from '../modules/requireProvider'
67✔
20
import { parse } from '../parser/parser'
67✔
21
import { AsyncScheduler, NonDetScheduler, PreemptiveScheduler } from '../schedulers'
67✔
22
import {
67✔
23
  callee,
24
  getEvaluationSteps,
25
  getRedex,
26
  IStepperPropContents,
27
  redexify
28
} from '../stepper/stepper'
29
import { sandboxedEval } from '../transpiler/evalContainer'
67✔
30
import { transpile } from '../transpiler/transpiler'
67✔
31
import { Context, RecursivePartial, Scheduler, Variant } from '../types'
67✔
32
import { forceIt } from '../utils/operators'
67✔
33
import { validateAndAnnotate } from '../validator/validator'
67✔
34
import { compileForConcurrent } from '../vm/svml-compiler'
67✔
35
import { runWithProgram } from '../vm/svml-machine'
67✔
36
import { determineExecutionMethod, hasVerboseErrors } from '.'
67✔
37
import { toSourceError } from './errors'
67✔
38
import { fullJSRunner } from './fullJSRunner'
67✔
39
import { determineVariant, resolvedErrorPromise } from './utils'
67✔
40

41
const DEFAULT_SOURCE_OPTIONS: Readonly<IOptions> = {
67✔
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
    wrapSourceModules: true,
54
    checkImports: true,
55
    loadTabs: true
56
  }
57
}
58

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

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

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

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

115
function runInterpreter(program: es.Program, context: Context, options: IOptions): Promise<Result> {
116
  let it = evaluate(program, context, true, true)
1,513✔
117
  let scheduler: Scheduler
118
  if (context.variant === Variant.NON_DET) {
1,513✔
119
    it = nonDetEvaluate(program, context)
486✔
120
    scheduler = new NonDetScheduler()
486✔
121
  } else if (options.scheduler === 'async') {
1,027✔
122
    scheduler = new AsyncScheduler()
4✔
123
  } else {
124
    scheduler = new PreemptiveScheduler(options.steps)
1,023✔
125
  }
126
  return scheduler.run(it, context)
1,513✔
127
}
128

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

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

159
    ;({ transpiled, sourceMapJson } = await transpile(
855✔
160
      transpiledProgram,
161
      context,
162
      options.importOptions
163
    ))
164
    let value = await sandboxedEval(transpiled, getRequireProvider(context), context.nativeStorage)
855✔
165

166
    if (context.variant === Variant.LAZY) {
785✔
167
      value = forceIt(value)
89✔
168
    }
169

170
    if (!options.isPrelude) {
785✔
171
      isPreviousCodeTimeoutError = false
473✔
172
    }
173

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

215
    const sourceError = await toSourceError(error, sourceMapJson)
×
216
    context.errors.push(sourceError)
×
217
    return resolvedErrorPromise
×
218
  }
219
}
220

221
function runECEvaluator(program: es.Program, context: Context, options: IOptions): Promise<Result> {
222
  const value = ECEvaluate(program, context, options)
201✔
223
  return ECEResultPromise(context, value)
201✔
224
}
225

226
export async function sourceRunner(
67✔
227
  program: es.Program,
228
  context: Context,
229
  isVerboseErrorsEnabled: boolean,
230
  options: RecursivePartial<IOptions> = {}
×
231
): Promise<Result> {
232
  // It is necessary to make a copy of the DEFAULT_SOURCE_OPTIONS object because merge()
233
  // will modify it rather than create a new object
234
  const theOptions = _.merge({ ...DEFAULT_SOURCE_OPTIONS }, options)
3,671✔
235
  context.variant = determineVariant(context, options)
3,671✔
236

237
  validateAndAnnotate(program, context)
3,671✔
238
  if (context.errors.length > 0) {
3,671✔
239
    return resolvedErrorPromise
11✔
240
  }
241

242
  if (context.variant === Variant.CONCURRENT) {
3,660✔
243
    return runConcurrent(program, context, theOptions)
103✔
244
  }
245

246
  if (theOptions.useSubst) {
3,557!
247
    return runSubstitution(program, context, theOptions)
×
248
  }
249

250
  determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
3,557✔
251

252
  if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
3,557✔
253
    return await fullJSRunner(program, context, theOptions.importOptions)
5✔
254
  }
255

256
  // All runners after this point evaluate the prelude.
257
  if (context.prelude !== null) {
3,552✔
258
    context.unTypecheckedCode.push(context.prelude)
983✔
259
    const prelude = parse(context.prelude, context)
983✔
260
    if (prelude === null) {
983!
261
      return resolvedErrorPromise
×
262
    }
263
    context.prelude = null
983✔
264
    await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
983✔
265
    return sourceRunner(program, context, isVerboseErrorsEnabled, options)
983✔
266
  }
267

268
  if (context.variant === Variant.EXPLICIT_CONTROL) {
2,569✔
269
    return runECEvaluator(program, context, theOptions)
201✔
270
  }
271

272
  if (context.executionMethod === 'ec-evaluator') {
2,368!
273
    if (options.isPrelude) {
×
274
      return runECEvaluator(
×
275
        program,
276
        { ...context, runtime: { ...context.runtime, debuggerOn: false } },
277
        theOptions
278
      )
279
    }
280
    return runECEvaluator(program, context, theOptions)
×
281
  }
282

283
  if (context.executionMethod === 'native') {
2,368✔
284
    return runNative(program, context, theOptions)
855✔
285
  }
286

287
  return runInterpreter(program!, context, theOptions)
1,513✔
288
}
289

290
export async function sourceFilesRunner(
67✔
291
  files: Partial<Record<string, string>>,
292
  entrypointFilePath: string,
293
  context: Context,
294
  options: RecursivePartial<IOptions> = {}
×
295
): Promise<Result> {
296
  const entrypointCode = files[entrypointFilePath]
1,920✔
297
  if (entrypointCode === undefined) {
1,920!
298
    context.errors.push(new CannotFindModuleError(entrypointFilePath))
×
299
    return resolvedErrorPromise
×
300
  }
301

302
  const isVerboseErrorsEnabled = hasVerboseErrors(entrypointCode)
1,920✔
303

304
  context.variant = determineVariant(context, options)
1,920✔
305
  // FIXME: The type checker does not support the typing of multiple files, so
306
  //        we only push the code in the entrypoint file. Ideally, all files
307
  //        involved in the program evaluation should be type-checked. Either way,
308
  //        the type checker is currently not used at all so this is not very
309
  //        urgent.
310
  context.unTypecheckedCode.push(entrypointCode)
1,920✔
311

312
  const currentCode = {
1,920✔
313
    files,
314
    entrypointFilePath
315
  }
316
  context.shouldIncreaseEvaluationTimeout = _.isEqual(previousCode, currentCode)
1,920✔
317
  previousCode = currentCode
1,920✔
318

319
  const preprocessedProgram = preprocessFileImports(files, entrypointFilePath, context)
1,920✔
320
  if (!preprocessedProgram) {
1,920✔
321
    return resolvedErrorPromise
215✔
322
  }
323
  context.previousPrograms.unshift(preprocessedProgram)
1,705✔
324

325
  return sourceRunner(preprocessedProgram, context, isVerboseErrorsEnabled, options)
1,705✔
326
}
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

© 2026 Coveralls, Inc