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

source-academy / js-slang / 13432398053

20 Feb 2025 09:46AM UTC coverage: 81.598% (-0.05%) from 81.652%
13432398053

Pull #1738

github

web-flow
Merge 89fbe92a8 into eaa71ce96
Pull Request #1738: bumping version

3648 of 4864 branches covered (75.0%)

Branch coverage included in aggregate %.

11468 of 13661 relevant lines covered (83.95%)

138746.04 hits per line

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

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

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

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

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

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

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

90
function runSubstitution(
91
  program: es.Program,
92
  context: Context,
93
  options: IOptions
94
): Promise<Result> {
95
  const steps = getEvaluationSteps(program, context, options)
×
96
  if (context.errors.length > 0) {
×
97
    return resolvedErrorPromise
×
98
  }
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, context)
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)
1,390✔
119
  let scheduler: Scheduler
120
  if (context.variant === Variant.NON_DET) {
1,390✔
121
    it = nonDetEvaluate(program, context)
486✔
122
    scheduler = new NonDetScheduler()
486✔
123
  } else if (options.scheduler === 'async') {
904!
124
    scheduler = new AsyncScheduler()
×
125
  } else {
126
    scheduler = new PreemptiveScheduler(options.steps)
904✔
127
  }
128
  return scheduler.run(it, context)
1,390✔
129
}
130

131
async function runNative(
132
  program: es.Program,
133
  context: Context,
134
  options: IOptions
135
): Promise<Result> {
136
  if (!options.isPrelude) {
766✔
137
    if (context.shouldIncreaseEvaluationTimeout && isPreviousCodeTimeoutError) {
449✔
138
      context.nativeStorage.maxExecTime *= JSSLANG_PROPERTIES.factorToIncreaseBy
3✔
139
    } else {
140
      context.nativeStorage.maxExecTime = options.originalMaxExecTime
446✔
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)
766✔
149
  let transpiled
150
  let sourceMapJson: RawSourceMap | undefined
151
  try {
766✔
152
    switch (context.variant) {
766!
153
      case Variant.GPU:
154
        transpileToGPU(transpiledProgram)
×
155
        break
×
156
      case Variant.LAZY:
157
        transpileToLazy(transpiledProgram)
97✔
158
        break
97✔
159
    }
160

161
    ;({ transpiled, sourceMapJson } = transpile(transpiledProgram, context))
766✔
162
    let value = sandboxedEval(transpiled, context.nativeStorage)
764✔
163

164
    if (context.variant === Variant.LAZY) {
696✔
165
      value = forceIt(value)
89✔
166
    }
167

168
    if (!options.isPrelude) {
696✔
169
      isPreviousCodeTimeoutError = false
379✔
170
    }
171

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

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

220
function runCSEMachine(program: es.Program, context: Context, options: IOptions): Promise<Result> {
221
  const value = CSEvaluate(program, context, options)
1,034✔
222
  return CSEResultPromise(context, value)
1,034✔
223
}
224

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

236
  if (
4,780✔
237
    context.chapter === Chapter.FULL_JS ||
14,313✔
238
    context.chapter === Chapter.FULL_TS ||
239
    context.chapter === Chapter.PYTHON_1
240
  ) {
241
    return fullJSRunner(program, context, theOptions.importOptions)
69✔
242
  }
243

244
  validateAndAnnotate(program, context)
4,711✔
245
  if (context.errors.length > 0) {
4,711✔
246
    return resolvedErrorPromise
11✔
247
  }
248

249
  if (context.variant === Variant.CONCURRENT) {
4,700✔
250
    return runConcurrent(program, context, theOptions)
103✔
251
  }
252

253
  if (theOptions.useSubst) {
4,597!
254
    return runSubstitution(program, context, theOptions)
×
255
  }
256

257
  determineExecutionMethod(theOptions, context, program, isVerboseErrorsEnabled)
4,597✔
258

259
  // native, don't evaluate prelude
260
  if (context.executionMethod === 'native' && context.variant === Variant.NATIVE) {
4,597✔
261
    return await fullJSRunner(program, context, theOptions.importOptions)
5✔
262
  }
263

264
  // All runners after this point evaluate the prelude.
265
  if (context.prelude !== null) {
4,592✔
266
    context.unTypecheckedCode.push(context.prelude)
1,402✔
267
    const prelude = parse(context.prelude, context)
1,402✔
268
    if (prelude === null) {
1,402!
269
      return resolvedErrorPromise
×
270
    }
271
    context.prelude = null
1,402✔
272
    await sourceRunner(prelude, context, isVerboseErrorsEnabled, { ...options, isPrelude: true })
1,402✔
273
    return sourceRunner(program, context, isVerboseErrorsEnabled, options)
1,402✔
274
  }
275

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

287
  if (context.executionMethod === 'native') {
2,156✔
288
    return runNative(program, context, theOptions)
766✔
289
  }
290

291
  return runInterpreter(program, context, theOptions)
1,390✔
292
}
293

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

314
  if (!preprocessResult.ok) {
2,190✔
315
    return {
214✔
316
      result: { status: 'error' },
317
      verboseErrors: preprocessResult.verboseErrors
318
    }
319
  }
320

321
  const { files, verboseErrors, program: preprocessedProgram } = preprocessResult
1,976✔
322

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

331
  const currentCode = {
1,976✔
332
    files,
333
    entrypointFilePath
334
  }
335
  context.shouldIncreaseEvaluationTimeout = _.isEqual(previousCode, currentCode)
1,976✔
336
  previousCode = currentCode
1,976✔
337

338
  context.previousPrograms.unshift(preprocessedProgram)
1,976✔
339

340
  const result = await sourceRunner(preprocessedProgram, context, verboseErrors, options)
1,976✔
341
  const resultMapper = mapResult(context)
1,976✔
342

343
  return {
1,976✔
344
    result: resultMapper(result),
345
    verboseErrors
346
  }
347
}
348

349
/**
350
 * Useful for just running a single line of code with the given context
351
 * However, if this single line of code is an import statement,
352
 * then the FileGetter is necessary, otherwise all local imports will
353
 * fail with ModuleNotFoundError
354
 */
355
export function runCodeInSource(
60✔
356
  code: string,
357
  context: Context,
358
  options: RecursivePartial<IOptions> = {},
13✔
359
  defaultFilePath: string = '/default.js',
362✔
360
  fileGetter?: FileGetter
361
) {
362
  return sourceFilesRunner(
384✔
363
    path => {
364
      if (path === defaultFilePath) return Promise.resolve(code)
385✔
365
      if (!fileGetter) return Promise.resolve(undefined)
1!
366
      return fileGetter(path)
1✔
367
    },
368
    defaultFilePath,
369
    context,
370
    options
371
  )
372
}
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