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

source-academy / js-slang / 4474907293

pending completion
4474907293

push

github

GitHub
Add multiple file compilation for SVML (#1375)

3244 of 4225 branches covered (76.78%)

Branch coverage included in aggregate %.

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

10150 of 11780 relevant lines covered (86.16%)

120350.69 hits per line

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

57.35
/src/index.ts
1
import { SourceLocation } from 'estree'
2
import { SourceMapConsumer } from 'source-map'
43✔
3

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

30
import { ECEResultPromise, resumeEvaluate } from './ec-evaluator/interpreter'
43✔
31
import { CannotFindModuleError } from './errors/localImportErrors'
43✔
32
import { validateFilePath } from './localImports/filePaths'
43✔
33
import preprocessFileImports from './localImports/preprocessor'
43✔
34
import { getKeywords, getProgramNames, NameDeclaration } from './name-extractor'
43✔
35
import { parse } from './parser/parser'
43✔
36
import { parseWithComments } from './parser/utils'
43✔
37
import {
43✔
38
  fullJSRunner,
39
  hasVerboseErrors,
40
  htmlRunner,
41
  resolvedErrorPromise,
42
  sourceFilesRunner
43
} from './runner'
44
import { typeCheck } from './typeChecker/typeChecker'
43✔
45
import { typeToString } from './utils/stringify'
43✔
46

47
export interface IOptions {
48
  scheduler: 'preemptive' | 'async'
49
  steps: number
50
  stepLimit: number
51
  executionMethod: ExecutionMethod
52
  variant: Variant
53
  originalMaxExecTime: number
54
  useSubst: boolean
55
  isPrelude: boolean
56
  throwInfiniteLoops: boolean
57
}
58

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

67
let verboseErrors: boolean = false
43✔
68

69
export function parseError(errors: SourceError[], verbose: boolean = verboseErrors): string {
43✔
70
  const errorMessagesArr = errors.map(error => {
1,630✔
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}] ` : ''
877!
73
    const line = error.location ? error.location.start.line : '<unknown>'
877!
74
    const column = error.location ? error.location.start.column : '<unknown>'
877!
75
    const explanation = error.explain()
877✔
76

77
    if (verbose) {
877✔
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()
133✔
81
      return line < 1
133✔
82
        ? `${filePath}${explanation}\n${elaboration}\n`
133✔
83
        : `${filePath}Line ${line}, Column ${column}: ${explanation}\n${elaboration}\n`
84
    } else {
85
      return line < 1 ? explanation : `${filePath}Line ${line}: ${explanation}`
744✔
86
    }
87
  })
88
  return errorMessagesArr.join('\n')
1,630✔
89
}
90

91
export function findDeclaration(
43✔
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(
43✔
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(
43✔
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(
43✔
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(
43✔
182
  code: string,
183
  line: number,
184
  col: number,
185
  context: Context
186
): Promise<[NameDeclaration[], boolean]> {
187
  const [program, comments] = parseWithComments(code)
29✔
188

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

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

199
export function getTypeInformation(
43✔
200
  code: string,
201
  context: Context,
202
  loc: { line: number; column: number },
203
  name: string
204
): string {
205
  try {
×
206
    // parse the program into typed nodes and parse error
207
    const program = typedParse(code, context)
×
208
    if (program === null) {
×
209
      return ''
×
210
    }
211
    if (context.prelude !== null) {
×
212
      typeCheck(typedParse(context.prelude, context)!, context)
×
213
    }
214
    const [typedProgram, error] = typeCheck(program, context)
×
215
    const parsedError = parseError(error)
×
216
    if (context.prelude !== null) {
×
217
      // the env of the prelude was added, we now need to remove it
218
      context.typeEnvironment.pop()
×
219
    }
220

221
    // initialize the ans string
222
    let ans = ''
×
223
    if (parsedError) {
×
224
      ans += parsedError + '\n'
×
225
    }
226
    if (!typedProgram) {
×
227
      return ans
×
228
    }
229

230
    // get name of the node
231
    const getName = (typedNode: NodeWithInferredType<es.Node>) => {
×
232
      let nodeId = ''
×
233
      if (typedNode.type) {
×
234
        if (typedNode.type === 'FunctionDeclaration') {
×
235
          if (typedNode.id === null) {
×
236
            throw new Error(
×
237
              'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.'
238
            )
239
          }
240
          nodeId = typedNode.id.name
×
241
        } else if (typedNode.type === 'VariableDeclaration') {
×
242
          nodeId = (typedNode.declarations[0].id as es.Identifier).name
×
243
        } else if (typedNode.type === 'Identifier') {
×
244
          nodeId = typedNode.name
×
245
        }
246
      }
247
      return nodeId
×
248
    }
249

250
    // callback function for findNodeAt function
251
    function findByLocationPredicate(t: string, nd: NodeWithInferredType<es.Node>) {
252
      if (!nd.inferredType) {
×
253
        return false
×
254
      }
255

256
      const isInLoc = (nodeLoc: SourceLocation): boolean => {
×
257
        return !(
×
258
          nodeLoc.start.line > loc.line ||
×
259
          nodeLoc.end.line < loc.line ||
260
          (nodeLoc.start.line === loc.line && nodeLoc.start.column > loc.column) ||
261
          (nodeLoc.end.line === loc.line && nodeLoc.end.column < loc.column)
262
        )
263
      }
264

265
      const location = nd.loc
×
266
      if (nd.type && location) {
×
267
        return getName(nd) === name && isInLoc(location)
×
268
      }
269
      return false
×
270
    }
271

272
    // report both as the type inference
273

274
    const res = findNodeAt(typedProgram, undefined, undefined, findByLocationPredicate)
×
275

276
    if (res === undefined) {
×
277
      return ans
×
278
    }
279

280
    const node: NodeWithInferredType<es.Node> = res.node
×
281

282
    if (node === undefined) {
×
283
      return ans
×
284
    }
285

286
    const actualNode =
287
      node.type === 'VariableDeclaration'
×
288
        ? (node.declarations[0].init! as NodeWithInferredType<es.Node>)
×
289
        : node
290
    const type = typeToString(
×
291
      actualNode.type === 'FunctionDeclaration'
292
        ? (actualNode as FuncDeclWithInferredTypeAnnotation).functionInferredType!
×
293
        : actualNode.inferredType!
294
    )
295
    return ans + `At Line ${loc.line} => ${getName(node)}: ${type}`
×
296
  } catch (error) {
297
    return ''
×
298
  }
299
}
300

301
export async function runInContext(
43✔
302
  code: string,
303
  context: Context,
304
  options: Partial<IOptions> = {}
47✔
305
): Promise<Result> {
306
  const defaultFilePath = '/default.js'
1,680✔
307
  const files: Partial<Record<string, string>> = {}
1,680✔
308
  files[defaultFilePath] = code
1,680✔
309
  return runFilesInContext(files, defaultFilePath, context, options)
1,680✔
310
}
311

312
export async function runFilesInContext(
43✔
313
  files: Partial<Record<string, string>>,
314
  entrypointFilePath: string,
315
  context: Context,
316
  options: Partial<IOptions> = {}
18✔
317
): Promise<Result> {
318
  for (const filePath in files) {
1,698✔
319
    const filePathError = validateFilePath(filePath)
1,706✔
320
    if (filePathError !== null) {
1,706✔
321
      context.errors.push(filePathError)
4✔
322
      return resolvedErrorPromise
4✔
323
    }
324
  }
325

326
  const code = files[entrypointFilePath]
1,694✔
327
  if (code === undefined) {
1,694✔
328
    context.errors.push(new CannotFindModuleError(entrypointFilePath))
2✔
329
    return resolvedErrorPromise
2✔
330
  }
331

332
  if (context.chapter === Chapter.FULL_JS) {
1,692✔
333
    const program = parse(code, context)
13✔
334
    if (program === null) {
13✔
335
      return resolvedErrorPromise
1✔
336
    }
337
    return fullJSRunner(program, context, options)
12✔
338
  }
339

340
  if (context.chapter === Chapter.HTML) {
1,679✔
341
    return htmlRunner(code, context, options)
1✔
342
  }
343

344
  // FIXME: Clean up state management so that the `parseError` function is pure.
345
  //        This is not a huge priority, but it would be good not to make use of
346
  //        global state.
347
  verboseErrors = hasVerboseErrors(code)
1,678✔
348

349
  return sourceFilesRunner(files, entrypointFilePath, context, options)
1,678✔
350
}
351

352
export function resume(result: Result): Finished | ResultError | Promise<Result> {
43✔
353
  if (result.status === 'finished' || result.status === 'error') {
455!
354
    return result
×
355
  } else if (result.status === 'suspended-ec-eval') {
455✔
356
    const value = resumeEvaluate(result.context)
11✔
357
    return ECEResultPromise(result.context, value)
11✔
358
  } else {
359
    return result.scheduler.run(result.it, result.context)
444✔
360
  }
361
}
362

363
export function interrupt(context: Context) {
43✔
364
  const globalEnvironment = context.runtime.environments[context.runtime.environments.length - 1]
×
365
  context.runtime.environments = [globalEnvironment]
×
366
  context.runtime.isRunning = false
×
367
  context.errors.push(new InterruptedError(context.runtime.nodes[0]))
×
368
}
369

370
export function compile(
43✔
371
  code: string,
372
  context: Context,
373
  vmInternalFunctions?: string[]
374
): SVMProgram | undefined {
375
  const defaultFilePath = '/default.js'
1✔
376
  const files: Partial<Record<string, string>> = {}
1✔
377
  files[defaultFilePath] = code
1✔
378
  return compileFiles(files, defaultFilePath, context, vmInternalFunctions)
1✔
379
}
380

381
export function compileFiles(
43✔
382
  files: Partial<Record<string, string>>,
383
  entrypointFilePath: string,
384
  context: Context,
385
  vmInternalFunctions?: string[]
386
): SVMProgram | undefined {
387
  for (const filePath in files) {
7✔
388
    const filePathError = validateFilePath(filePath)
9✔
389
    if (filePathError !== null) {
9✔
390
      context.errors.push(filePathError)
4✔
391
      return undefined
4✔
392
    }
393
  }
394

395
  const entrypointCode = files[entrypointFilePath]
3✔
396
  if (entrypointCode === undefined) {
3✔
397
    context.errors.push(new CannotFindModuleError(entrypointFilePath))
2✔
398
    return undefined
2✔
399
  }
400

401
  const preprocessedProgram = preprocessFileImports(files, entrypointFilePath, context)
1✔
402
  if (!preprocessedProgram) {
1!
403
    return undefined
×
404
  }
405

406
  try {
1✔
407
    return compileToIns(preprocessedProgram, undefined, vmInternalFunctions)
1✔
408
  } catch (error) {
409
    context.errors.push(error)
×
410
    return undefined
×
411
  }
412
}
413

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