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

source-academy / js-slang / 11296882442

11 Oct 2024 05:41PM UTC coverage: 81.253% (-0.4%) from 81.61%
11296882442

Pull #1725

github

web-flow
Merge cabdfe168 into 883ffe766
Pull Request #1725: Remove Non-Det Interpreter

3379 of 4524 branches covered (74.69%)

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.

10664 of 12759 relevant lines covered (83.58%)

142972.81 hits per line

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

71.12
/src/index.ts
1
import { SourceLocation } from 'estree'
2
import * as es from 'estree'
3
import { SourceMapConsumer } from 'source-map'
54✔
4

5
import createContext from './createContext'
54✔
6
import { InterruptedError } from './errors/errors'
54✔
7
import { findDeclarationNode, findIdentifierNode } from './finder'
54✔
8
import { looseParse, parseWithComments } from './parser/utils'
54✔
9
import { getAllOccurrencesInScopeHelper, getScopeHelper } from './scope-refactoring'
54✔
10
import { setBreakpointAtLine } from './stdlib/inspector'
54✔
11
import {
54✔
12
  Chapter,
13
  Context,
14
  Error as ResultError,
15
  ExecutionMethod,
16
  Finished,
17
  ModuleContext,
18
  RecursivePartial,
19
  Result,
20
  SourceError,
21
  SVMProgram,
22
  Variant
23
} from './types'
24
import { assemble } from './vm/svml-assembler'
54✔
25
import { compileToIns } from './vm/svml-compiler'
54✔
26
export { SourceDocumentation } from './editors/ace/docTooltip'
58✔
27

28
import { CSEResultPromise, resumeEvaluate } from './cse-machine/interpreter'
54✔
29
import { ModuleNotFoundError } from './modules/errors'
54✔
30
import type { ImportOptions } from './modules/moduleTypes'
31
import preprocessFileImports from './modules/preprocessor'
54✔
32
import { validateFilePath } from './modules/preprocessor/filePaths'
54✔
33
import { getKeywords, getProgramNames, NameDeclaration } from './name-extractor'
54✔
34
import { htmlRunner, resolvedErrorPromise, sourceFilesRunner } from './runner'
54✔
35

36
export interface IOptions {
37
  scheduler: 'preemptive' | 'async'
38
  steps: number
39
  stepLimit: number
40
  executionMethod: ExecutionMethod
41
  variant: Variant
42
  originalMaxExecTime: number
43
  useSubst: boolean
44
  isPrelude: boolean
45
  throwInfiniteLoops: boolean
46
  envSteps: number
47

48
  importOptions: ImportOptions
49

50
  /**
51
   * Set this to true if source file information should be
52
   * added when parsing programs into ASTs
53
   *
54
   * Set to null to let js-slang decide automatically
55
   */
56
  shouldAddFileName: boolean | null
57
}
58

59
// needed to work on browsers
60
if (typeof window !== 'undefined') {
54✔
61
  // @ts-ignore
62
  SourceMapConsumer.initialize({
54✔
63
    'lib/mappings.wasm': 'https://unpkg.com/source-map@0.7.3/lib/mappings.wasm'
64
  })
65
}
66

67
let verboseErrors: boolean = false
54✔
68

69
export function parseError(errors: SourceError[], verbose: boolean = verboseErrors): string {
54✔
70
  const errorMessagesArr = errors.map(error => {
1,572✔
71
    // FIXME: Either refactor the parser to output an ESTree-compliant AST, or modify the ESTree types.
72
    const filePath = error.location?.source ? `[${error.location.source}] ` : ''
775✔
73
    const line = error.location ? error.location.start.line : '<unknown>'
775!
74
    const column = error.location ? error.location.start.column : '<unknown>'
775!
75
    const explanation = error.explain()
775✔
76

77
    if (verbose) {
775✔
78
      // TODO currently elaboration is just tagged on to a new line after the error message itself. find a better
79
      // way to display it.
80
      const elaboration = error.elaborate()
134✔
81
      return line < 1
134✔
82
        ? `${filePath}${explanation}\n${elaboration}\n`
83
        : `${filePath}Line ${line}, Column ${column}: ${explanation}\n${elaboration}\n`
84
    } else {
85
      return line < 1 ? explanation : `${filePath}Line ${line}: ${explanation}`
641✔
86
    }
87
  })
88
  return errorMessagesArr.join('\n')
1,572✔
89
}
90

91
export function findDeclaration(
54✔
92
  code: string,
93
  context: Context,
94
  loc: { line: number; column: number }
95
): SourceLocation | null | undefined {
96
  const program = looseParse(code, context)
16✔
97
  if (!program) {
16!
98
    return null
×
99
  }
100
  const identifierNode = findIdentifierNode(program, context, loc)
16✔
101
  if (!identifierNode) {
16✔
102
    return null
1✔
103
  }
104
  const declarationNode = findDeclarationNode(program, identifierNode)
15✔
105
  if (!declarationNode || identifierNode === declarationNode) {
15✔
106
    return null
1✔
107
  }
108
  return declarationNode.loc
14✔
109
}
110

111
export function getScope(
54✔
112
  code: string,
113
  context: Context,
114
  loc: { line: number; column: number }
115
): SourceLocation[] {
116
  const program = looseParse(code, context)
6✔
117
  if (!program) {
6!
118
    return []
×
119
  }
120
  const identifierNode = findIdentifierNode(program, context, loc)
6✔
121
  if (!identifierNode) {
6!
122
    return []
×
123
  }
124
  const declarationNode = findDeclarationNode(program, identifierNode)
6✔
125
  if (!declarationNode || declarationNode.loc == null || identifierNode !== declarationNode) {
6!
126
    return []
×
127
  }
128

129
  return getScopeHelper(declarationNode.loc, program, identifierNode.name)
6✔
130
}
131

132
export function getAllOccurrencesInScope(
54✔
133
  code: string,
134
  context: Context,
135
  loc: { line: number; column: number }
136
): SourceLocation[] {
137
  const program = looseParse(code, context)
12✔
138
  if (!program) {
12!
139
    return []
×
140
  }
141
  const identifierNode = findIdentifierNode(program, context, loc)
12✔
142
  if (!identifierNode) {
12!
143
    return []
×
144
  }
145
  const declarationNode = findDeclarationNode(program, identifierNode)
12✔
146
  if (declarationNode == null || declarationNode.loc == null) {
12!
147
    return []
×
148
  }
149
  return getAllOccurrencesInScopeHelper(declarationNode.loc, program, identifierNode.name)
12✔
150
}
151

152
export function hasDeclaration(
54✔
153
  code: string,
154
  context: Context,
155
  loc: { line: number; column: number }
156
): boolean {
157
  const program = looseParse(code, context)
×
158
  if (!program) {
×
159
    return false
×
160
  }
161
  const identifierNode = findIdentifierNode(program, context, loc)
×
162
  if (!identifierNode) {
×
163
    return false
×
164
  }
165
  const declarationNode = findDeclarationNode(program, identifierNode)
×
166
  if (declarationNode == null || declarationNode.loc == null) {
×
167
    return false
×
168
  }
169

170
  return true
×
171
}
172

173
/**
174
 * Gets names present within a string of code
175
 * @param code Code to parse
176
 * @param line Line position of the cursor
177
 * @param col Column position of the cursor
178
 * @param context Evaluation context
179
 * @returns `[NameDeclaration[], true]` if suggestions should be displayed, `[[], false]` otherwise
180
 */
181
export async function getNames(
54✔
182
  code: string,
183
  line: number,
184
  col: number,
185
  context: Context
186
): Promise<[NameDeclaration[], boolean]> {
187
  const [program, comments] = parseWithComments(code)
44✔
188

189
  if (!program) {
44!
190
    return [[], false]
×
191
  }
192
  const cursorLoc: es.Position = { line, column: col }
44✔
193

194
  const [progNames, displaySuggestions] = await getProgramNames(program, comments, cursorLoc)
44✔
195
  const keywords = getKeywords(program, cursorLoc, context)
44✔
196
  return [progNames.concat(keywords), displaySuggestions]
44✔
197
}
198

199
export async function runInContext(
54✔
200
  code: string,
201
  context: Context,
202
  options: RecursivePartial<IOptions> = {}
49✔
203
): Promise<Result> {
204
  const defaultFilePath = '/default.js'
1,504✔
205
  const files: Partial<Record<string, string>> = {}
1,504✔
206
  files[defaultFilePath] = code
1,504✔
207
  return runFilesInContext(files, defaultFilePath, context, options)
1,504✔
208
}
209

210
// this is the first entrypoint for all source files.
211
// as such, all mapping functions required by alternate languages
212
// should be defined here.
213
export async function runFilesInContext(
54✔
214
  files: Partial<Record<string, string>>,
215
  entrypointFilePath: string,
216
  context: Context,
217
  options: RecursivePartial<IOptions> = {}
6✔
218
): Promise<Result> {
219
  for (const filePath in files) {
1,510✔
220
    const filePathError = validateFilePath(filePath)
1,512✔
221
    if (filePathError !== null) {
1,512✔
222
      context.errors.push(filePathError)
4✔
223
      return resolvedErrorPromise
4✔
224
    }
225
  }
226

227
  let result: Result
228
  if (context.chapter === Chapter.HTML) {
1,506✔
229
    const code = files[entrypointFilePath]
1✔
230
    if (code === undefined) {
1!
231
      context.errors.push(new ModuleNotFoundError(entrypointFilePath))
×
232
      return resolvedErrorPromise
×
233
    }
234
    result = await htmlRunner(code, context, options)
1✔
235
  } else {
236
    // FIXME: Clean up state management so that the `parseError` function is pure.
237
    //        This is not a huge priority, but it would be good not to make use of
238
    //        global state.
239
    ;({ result, verboseErrors } = await sourceFilesRunner(
1,505✔
240
      p => Promise.resolve(files[p]),
1,505✔
241
      entrypointFilePath,
242
      context,
243
      {
244
        ...options,
245
        shouldAddFileName: options.shouldAddFileName ?? Object.keys(files).length > 1
3,010✔
246
      }
247
    ))
248
  }
249

250
  return result
1,506✔
251
}
252

253
export function resume(result: Result): Finished | ResultError | Promise<Result> {
54✔
UNCOV
254
  if (result.status === 'finished' || result.status === 'error') {
×
255
    return result
×
UNCOV
256
  } else if (result.status === 'suspended-cse-eval') {
×
257
    const value = resumeEvaluate(result.context)
×
258
    return CSEResultPromise(result.context, value)
×
259
  } else {
UNCOV
260
    return result.scheduler.run(result.it, result.context)
×
261
  }
262
}
263

264
export function interrupt(context: Context) {
54✔
265
  const globalEnvironment = context.runtime.environments[context.runtime.environments.length - 1]
×
266
  context.runtime.environments = [globalEnvironment]
×
267
  context.runtime.isRunning = false
×
268
  context.errors.push(new InterruptedError(context.runtime.nodes[0]))
×
269
}
270

271
export function compile(
54✔
272
  code: string,
273
  context: Context,
274
  vmInternalFunctions?: string[]
275
): Promise<SVMProgram | undefined> {
276
  const defaultFilePath = '/default.js'
1✔
277
  const files: Partial<Record<string, string>> = {}
1✔
278
  files[defaultFilePath] = code
1✔
279
  return compileFiles(files, defaultFilePath, context, vmInternalFunctions)
1✔
280
}
281

282
export async function compileFiles(
54✔
283
  files: Partial<Record<string, string>>,
284
  entrypointFilePath: string,
285
  context: Context,
286
  vmInternalFunctions?: string[]
287
): Promise<SVMProgram | undefined> {
288
  for (const filePath in files) {
7✔
289
    const filePathError = validateFilePath(filePath)
9✔
290
    if (filePathError !== null) {
9✔
291
      context.errors.push(filePathError)
4✔
292
      return undefined
4✔
293
    }
294
  }
295

296
  const preprocessResult = await preprocessFileImports(
3✔
297
    p => Promise.resolve(files[p]),
3✔
298
    entrypointFilePath,
299
    context,
300
    { shouldAddFileName: Object.keys(files).length > 1 }
301
  )
302

303
  if (!preprocessResult.ok) {
3✔
304
    return undefined
2✔
305
  }
306

307
  try {
1✔
308
    return compileToIns(preprocessResult.program, undefined, vmInternalFunctions)
1✔
309
  } catch (error) {
310
    context.errors.push(error)
×
311
    return undefined
×
312
  }
313
}
314

315
export { createContext, Context, ModuleContext, Result, setBreakpointAtLine, assemble }
58✔
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