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

source-academy / js-slang / 5710515365

pending completion
5710515365

push

github

web-flow
Fix stepper: not display runtime errors (#1458)

3593 of 4712 branches covered (76.25%)

Branch coverage included in aggregate %.

2 of 2 new or added lines in 1 file covered. (100.0%)

10699 of 12390 relevant lines covered (86.35%)

102155.4 hits per line

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

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

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

41
const DEFAULT_SOURCE_OPTIONS: IOptions = {
66✔
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
}
53

54
let previousCode: {
55
  files: Partial<Record<string, string>>
56
  entrypointFilePath: string
57
} | null = null
66✔
58
let isPreviousCodeTimeoutError = false
66✔
59

60
function runConcurrent(program: es.Program, context: Context, options: IOptions): Promise<Result> {
61
  if (context.shouldIncreaseEvaluationTimeout) {
103!
62
    context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
×
63
  } else {
64
    context.nativeStorage.maxExecTime = options.originalMaxExecTime
103✔
65
  }
66

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

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

110
function runInterpreter(program: es.Program, context: Context, options: IOptions): Promise<Result> {
111
  let it = evaluate(program, context, true, true)
1,505✔
112
  let scheduler: Scheduler
113
  if (context.variant === Variant.NON_DET) {
1,505✔
114
    it = nonDetEvaluate(program, context)
486✔
115
    scheduler = new NonDetScheduler()
486✔
116
  } else if (options.scheduler === 'async') {
1,019✔
117
    scheduler = new AsyncScheduler()
4✔
118
  } else {
119
    scheduler = new PreemptiveScheduler(options.steps)
1,015✔
120
  }
121
  return scheduler.run(it, context)
1,505✔
122
}
123

124
async function runNative(
125
  program: es.Program,
126
  context: Context,
127
  options: IOptions
128
): Promise<Result> {
129
  if (!options.isPrelude) {
845✔
130
    if (context.shouldIncreaseEvaluationTimeout && isPreviousCodeTimeoutError) {
538✔
131
      context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
3✔
132
    } else {
133
      context.nativeStorage.maxExecTime = options.originalMaxExecTime
535✔
134
    }
135
  }
136

137
  // For whatever reason, the transpiler mutates the state of the AST as it is transpiling and inserts
138
  // a bunch of global identifiers to it. Once that happens, the infinite loop detection instrumentation
139
  // ends up generating code that has syntax errors. As such, we need to make a deep copy here to preserve
140
  // the original AST for future use, such as with the infinite loop detector.
141
  const transpiledProgram = _.cloneDeep(program)
845✔
142
  let transpiled
143
  let sourceMapJson: RawSourceMap | undefined
144
  try {
845✔
145
    appendModulesToContext(transpiledProgram, context)
845✔
146

147
    switch (context.variant) {
845✔
148
      case Variant.GPU:
97!
149
        transpileToGPU(transpiledProgram)
×
150
        break
×
151
      case Variant.LAZY:
152
        transpileToLazy(transpiledProgram)
97✔
153
        break
97✔
154
    }
155

156
    ;({ transpiled, sourceMapJson } = transpile(transpiledProgram, context))
845✔
157
    let value = await sandboxedEval(transpiled, getRequireProvider(context), context.nativeStorage)
845✔
158

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

163
    if (!options.isPrelude) {
775✔
164
      isPreviousCodeTimeoutError = false
468✔
165
    }
166

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

205
    const sourceError: SourceError = await toSourceError(error, sourceMapJson)
×
206
    context.errors.push(sourceError)
×
207
    return resolvedErrorPromise
×
208
  }
209
}
210

211
function runECEvaluator(program: es.Program, context: Context, options: IOptions): Promise<Result> {
212
  const value = ECEvaluate(program, context, options)
201✔
213
  return ECEResultPromise(context, value)
201✔
214
}
215

216
export async function sourceRunner(
66✔
217
  program: es.Program,
218
  context: Context,
219
  isVerboseErrorsEnabled: boolean,
220
  options: Partial<IOptions> = {}
×
221
): Promise<Result> {
222
  const theOptions: IOptions = { ...DEFAULT_SOURCE_OPTIONS, ...options }
3,644✔
223
  context.variant = determineVariant(context, options)
3,644✔
224

225
  validateAndAnnotate(program, context)
3,644✔
226
  if (context.errors.length > 0) {
3,644✔
227
    return resolvedErrorPromise
11✔
228
  }
229

230
  if (context.variant === Variant.CONCURRENT) {
3,633✔
231
    return runConcurrent(program, context, theOptions)
103✔
232
  }
233

234
  if (theOptions.useSubst) {
3,530!
235
    return runSubstitution(program, context, theOptions)
×
236
  }
237

238
  determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
3,530✔
239

240
  if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
3,530✔
241
    return await fullJSRunner(program, context, theOptions)
5✔
242
  }
243

244
  // All runners after this point evaluate the prelude.
245
  if (context.prelude !== null) {
3,525✔
246
    context.unTypecheckedCode.push(context.prelude)
974✔
247
    const prelude = parse(context.prelude, context)
974✔
248
    if (prelude === null) {
974!
249
      return resolvedErrorPromise
×
250
    }
251
    context.prelude = null
974✔
252
    await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
974✔
253
    return sourceRunner(program, context, isVerboseErrorsEnabled, options)
974✔
254
  }
255

256
  if (context.variant === Variant.EXPLICIT_CONTROL) {
2,551✔
257
    return runECEvaluator(program, context, theOptions)
201✔
258
  }
259

260
  if (context.executionMethod === 'ec-evaluator') {
2,350!
261
    if (options.isPrelude) {
×
262
      return runECEvaluator(
×
263
        program,
264
        { ...context, runtime: { ...context.runtime, debuggerOn: false } },
265
        theOptions
266
      )
267
    }
268
    return runECEvaluator(program, context, theOptions)
×
269
  }
270

271
  if (context.executionMethod === 'native') {
2,350✔
272
    return runNative(program, context, theOptions)
845✔
273
  }
274

275
  return runInterpreter(program!, context, theOptions)
1,505✔
276
}
277

278
export async function sourceFilesRunner(
66✔
279
  files: Partial<Record<string, string>>,
280
  entrypointFilePath: string,
281
  context: Context,
282
  options: Partial<IOptions> = {}
×
283
): Promise<Result> {
284
  const entrypointCode = files[entrypointFilePath]
1,911✔
285
  if (entrypointCode === undefined) {
1,911!
286
    context.errors.push(new CannotFindModuleError(entrypointFilePath))
×
287
    return resolvedErrorPromise
×
288
  }
289

290
  const isVerboseErrorsEnabled = hasVerboseErrors(entrypointCode)
1,911✔
291

292
  context.variant = determineVariant(context, options)
1,911✔
293
  // FIXME: The type checker does not support the typing of multiple files, so
294
  //        we only push the code in the entrypoint file. Ideally, all files
295
  //        involved in the program evaluation should be type-checked. Either way,
296
  //        the type checker is currently not used at all so this is not very
297
  //        urgent.
298
  context.unTypecheckedCode.push(entrypointCode)
1,911✔
299

300
  const currentCode = {
1,911✔
301
    files,
302
    entrypointFilePath
303
  }
304
  context.shouldIncreaseEvaluationTimeout = _.isEqual(previousCode, currentCode)
1,911✔
305
  previousCode = currentCode
1,911✔
306

307
  const preprocessedProgram = preprocessFileImports(files, entrypointFilePath, context)
1,911✔
308
  if (!preprocessedProgram) {
1,911✔
309
    return resolvedErrorPromise
215✔
310
  }
311
  context.previousPrograms.unshift(preprocessedProgram)
1,696✔
312

313
  return sourceRunner(preprocessedProgram, context, isVerboseErrorsEnabled, options)
1,696✔
314
}
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