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

source-academy / js-slang / 5406352152

pending completion
5406352152

Pull #1428

github

web-flow
Merge 0380f5ed7 into 8618e26e4
Pull Request #1428: Further Enhancements to the Module System

3611 of 4728 branches covered (76.37%)

Branch coverage included in aggregate %.

831 of 831 new or added lines in 50 files covered. (100.0%)

10852 of 12603 relevant lines covered (86.11%)

93898.15 hits per line

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

82.46
/src/runner/sourceRunner.ts
1
import { generate } from 'astring'
51✔
2
import * as es from 'estree'
3
import * as _ from 'lodash'
51✔
4
import { RawSourceMap } from 'source-map'
5

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

42
const DEFAULT_SOURCE_OPTIONS: IOptions = {
51✔
43
  scheduler: 'async',
44
  steps: 1000,
45
  stepLimit: 1000,
46
  executionMethod: 'auto',
47
  variant: Variant.DEFAULT,
48
  originalMaxExecTime: 1000,
49
  useSubst: false,
50
  isPrelude: false,
51
  throwInfiniteLoops: true,
52

53
  logTranspilerOutput: false,
54
  logPreprocessorOutput: true,
55
  importOptions: {
56
    loadTabs: true,
57
    wrapModules: true,
58
    allowUndefinedImports: false,
59
    resolveDirectories: false,
60
    resolveExtensions: null
61
  }
62
}
63

64
let previousCode: {
65
  files: Partial<Record<string, string>>
66
  entrypointFilePath: string
67
} | null = null
51✔
68
let isPreviousCodeTimeoutError = false
51✔
69

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

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

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

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

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

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

161
    ;({ transpiled, sourceMapJson } = await transpile(
853✔
162
      transpiledProgram,
163
      context,
164
      options.importOptions
165
    ))
166

167
    // if (!options.isPrelude) console.log(transpiled)
168

169
    if (options.logTranspilerOutput) console.log(transpiled)
853!
170
    let value = await sandboxedEval(transpiled, getRequireProvider(context), context.nativeStorage)
853✔
171

172
    if (context.variant === Variant.LAZY) {
783✔
173
      value = forceIt(value)
89✔
174
    }
175

176
    if (!options.isPrelude) {
783✔
177
      isPreviousCodeTimeoutError = false
472✔
178
    }
179

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

222
    const sourceError: SourceError = await toSourceError(error, sourceMapJson)
×
223
    context.errors.push(sourceError)
×
224
    return resolvedErrorPromise
×
225
  }
226
}
227

228
function runECEvaluator(program: es.Program, context: Context, options: IOptions): Promise<Result> {
229
  const value = ECEvaluate(program, context, options)
198✔
230
  return ECEResultPromise(context, value)
198✔
231
}
232

233
export async function sourceRunner(
51✔
234
  program: es.Program,
235
  context: Context,
236
  isVerboseErrorsEnabled: boolean,
237
  options: RecursivePartial<IOptions> = {}
×
238
): Promise<Result> {
239
  const theOptions: IOptions = {
3,667✔
240
    ...DEFAULT_SOURCE_OPTIONS,
241
    ...options,
242
    importOptions: {
243
      ...DEFAULT_SOURCE_OPTIONS.importOptions,
244
      ...(options?.importOptions ?? {})
22,002!
245
    }
246
  }
247
  if (context.chapter === Chapter.FULL_JS) {
3,667✔
248
    return fullJSRunner(program, context, theOptions)
12✔
249
  }
250

251
  context.variant = determineVariant(context, options)
3,655✔
252

253
  validateAndAnnotate(program, context)
3,655✔
254
  if (context.errors.length > 0) {
3,655✔
255
    return resolvedErrorPromise
11✔
256
  }
257

258
  if (context.variant === Variant.CONCURRENT) {
3,644✔
259
    return runConcurrent(program, context, theOptions)
103✔
260
  }
261

262
  if (theOptions.useSubst) {
3,541!
263
    return runSubstitution(program, context, theOptions)
×
264
  }
265

266
  determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
3,541✔
267

268
  if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
3,541!
269
    return fullJSRunner(program, context, theOptions)
×
270
  }
271

272
  // All runners after this point evaluate the prelude.
273
  if (context.prelude !== null) {
3,541✔
274
    context.unTypecheckedCode.push(context.prelude)
980✔
275
    const prelude = parse(context.prelude, context)
980✔
276
    if (prelude === null) {
980!
277
      return resolvedErrorPromise
×
278
    }
279
    context.prelude = null
980✔
280
    await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
980✔
281
    return sourceRunner(program, context, isVerboseErrorsEnabled, options)
980✔
282
  }
283

284
  if (context.variant === Variant.EXPLICIT_CONTROL) {
2,561✔
285
    return runECEvaluator(program, context, theOptions)
198✔
286
  }
287

288
  if (context.executionMethod === 'ec-evaluator') {
2,363!
289
    if (options.isPrelude) {
×
290
      return runECEvaluator(
×
291
        program,
292
        { ...context, runtime: { ...context.runtime, debuggerOn: false } },
293
        theOptions
294
      )
295
    }
296
    return runECEvaluator(program, context, theOptions)
×
297
  }
298

299
  if (context.executionMethod === 'native') {
2,363✔
300
    return runNative(program, context, theOptions)
853✔
301
  }
302

303
  return runInterpreter(program, context, theOptions)
1,510✔
304
}
305

306
export async function sourceFilesRunner(
51✔
307
  files: Partial<Record<string, string>>,
308
  entrypointFilePath: string,
309
  context: Context,
310
  options: RecursivePartial<IOptions> = {}
×
311
): Promise<Result> {
312
  const entrypointCode = files[entrypointFilePath]
1,924✔
313
  if (entrypointCode === undefined) {
1,924!
314
    context.errors.push(new ModuleNotFoundError(entrypointFilePath))
×
315
    return resolvedErrorPromise
×
316
  }
317

318
  const isVerboseErrorsEnabled = hasVerboseErrors(entrypointCode)
1,924✔
319

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

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

335
  const preprocessedProgram = await preprocessFileImports(
1,924✔
336
    files,
337
    entrypointFilePath,
338
    context,
339
    options.importOptions
340
  )
341
  if (!preprocessedProgram) {
1,924✔
342
    return resolvedErrorPromise
217✔
343
  }
344

345
  if (options.logPreprocessorOutput) {
1,707!
346
    console.log(generate(preprocessedProgram))
×
347
  }
348
  context.previousPrograms.unshift(preprocessedProgram)
1,707✔
349

350
  return sourceRunner(preprocessedProgram, context, isVerboseErrorsEnabled, options)
1,707✔
351
}
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