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

source-academy / js-slang / 11929582114

20 Nov 2024 08:33AM UTC coverage: 81.654% (+0.04%) from 81.61%
11929582114

Pull #1728

github

web-flow
Merge c3f0269a5 into 665233634
Pull Request #1728: Implement the CSET machine, along with macros

3654 of 4864 branches covered (75.12%)

Branch coverage included in aggregate %.

491 of 596 new or added lines in 11 files covered. (82.38%)

1 existing line in 1 file now uncovered.

11474 of 13663 relevant lines covered (83.98%)

143610.35 hits per line

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

94.66
/src/cse-machine/interpreter.ts
1
/**
2
 * This interpreter implements an explicit-control evaluator.
3
 *
4
 * Heavily adapted from https://github.com/source-academy/JSpike/
5
 * and the legacy interpreter at '../interpreter/interpreter'
6
 */
7

8
/* tslint:disable:max-classes-per-file */
9
import * as es from 'estree'
10
import { isArray, reverse } from 'lodash'
79✔
11

12
import { IOptions } from '..'
13
import { UNKNOWN_LOCATION } from '../constants'
79✔
14
import * as errors from '../errors/errors'
79✔
15
import { RuntimeSourceError } from '../errors/runtimeSourceError'
79✔
16
import { checkEditorBreakpoints } from '../stdlib/inspector'
79✔
17
import { Context, ContiguousArrayElements, Result, Value, type StatementSequence } from '../types'
18
import * as ast from '../utils/ast/astCreator'
79✔
19
import { filterImportDeclarations } from '../utils/ast/helpers'
79✔
20
import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators'
79✔
21
import * as rttc from '../utils/rttc'
79✔
22
import * as seq from '../utils/statementSeqTransform'
79✔
23
import { checkProgramForUndefinedVariables } from '../validator/validator'
79✔
24
import Closure from './closure'
79✔
25
import {
79✔
26
  Continuation,
27
  isCallWithCurrentContinuation,
28
  makeDummyContCallExpression
29
} from './continuations'
30
import * as instr from './instrCreator'
79✔
31
import { Stack } from './stack'
79✔
32
import {
79✔
33
  AppInstr,
34
  ArrLitInstr,
35
  AssmtInstr,
36
  BinOpInstr,
37
  BranchInstr,
38
  CSEBreak,
39
  ControlItem,
40
  CseError,
41
  EnvInstr,
42
  ForInstr,
43
  Instr,
44
  InstrType,
45
  UnOpInstr,
46
  WhileInstr
47
} from './types'
48
import {
79✔
49
  checkNumberOfArguments,
50
  checkStackOverFlow,
51
  createBlockEnvironment,
52
  createEnvironment,
53
  createProgramEnvironment,
54
  currentEnvironment,
55
  currentTransformers,
56
  declareFunctionsAndVariables,
57
  declareIdentifier,
58
  defineVariable,
59
  envChanging,
60
  getVariable,
61
  handleArrayCreation,
62
  handleRuntimeError,
63
  handleSequence,
64
  hasBreakStatement,
65
  hasContinueStatement,
66
  hasDeclarations,
67
  hasImportDeclarations,
68
  isBlockStatement,
69
  isEnvArray,
70
  isEnvDependent,
71
  isInstr,
72
  isNode,
73
  isSimpleFunction,
74
  isStreamFn,
75
  popEnvironment,
76
  pushEnvironment,
77
  reduceConditional,
78
  setTransformers,
79
  setVariable,
80
  valueProducing
81
} from './utils'
82
import { isApply, isEval, schemeEval } from './scheme-macros'
79✔
83
import { Transformer } from './patterns'
84
import { isSchemeLanguage } from '../alt-langs/mapper'
79✔
85
import { flattenList, isList } from './macro-utils'
79✔
86

87
type CmdEvaluator = (
88
  command: ControlItem,
89
  context: Context,
90
  control: Control,
91
  stash: Stash,
92
  isPrelude: boolean
93
) => void
94

95
/**
96
 * The control is a list of commands that still needs to be executed by the machine.
97
 * It contains syntax tree nodes or instructions.
98
 */
99
export class Control extends Stack<ControlItem> {
79✔
100
  private numEnvDependentItems: number
101
  public constructor(program?: es.Program | StatementSequence) {
102
    super()
1,055✔
103
    this.numEnvDependentItems = 0
1,055✔
104
    // Load program into control stack
105
    program ? this.push(program) : null
1,055✔
106
  }
107

108
  public canAvoidEnvInstr(): boolean {
109
    return this.numEnvDependentItems === 0
532,221✔
110
  }
111

112
  // For testing purposes
113
  public getNumEnvDependentItems(): number {
114
    return this.numEnvDependentItems
1,369✔
115
  }
116

117
  public pop(): ControlItem | undefined {
118
    const item = super.pop()
7,580,065✔
119
    if (item !== undefined && isEnvDependent(item)) {
7,580,065✔
120
      this.numEnvDependentItems--
5,064,970✔
121
    }
122
    return item
7,580,065✔
123
  }
124

125
  public push(...items: ControlItem[]): void {
126
    const itemsNew: ControlItem[] = Control.simplifyBlocksWithoutDeclarations(...items)
7,542,938✔
127
    itemsNew.forEach((item: ControlItem) => {
7,542,938✔
128
      if (isEnvDependent(item)) {
7,981,949✔
129
        this.numEnvDependentItems++
5,066,220✔
130
      }
131
    })
132
    super.push(...itemsNew)
7,542,938✔
133
  }
134

135
  /**
136
   * Before pushing block statements on the control stack, we check if the block statement has any declarations.
137
   * If not, the block is converted to a StatementSequence.
138
   * @param items The items being pushed on the control.
139
   * @returns The same set of control items, but with block statements without declarations converted to StatementSequences.
140
   * NOTE: this function handles any case where StatementSequence has to be converted back into BlockStatement due to type issues
141
   */
142
  private static simplifyBlocksWithoutDeclarations(...items: ControlItem[]): ControlItem[] {
143
    const itemsNew: ControlItem[] = []
7,542,938✔
144
    items.forEach(item => {
7,542,938✔
145
      if (isNode(item) && isBlockStatement(item) && !hasDeclarations(item)) {
7,981,949✔
146
        // Push block body as statement sequence
147
        const seq: StatementSequence = ast.statementSequence(item.body, item.loc)
261,972✔
148
        itemsNew.push(seq)
261,972✔
149
      } else {
150
        itemsNew.push(item)
7,719,977✔
151
      }
152
    })
153
    return itemsNew
7,542,938✔
154
  }
155

156
  public copy(): Control {
157
    const newControl = new Control()
16✔
158
    const stackCopy = super.getStack()
16✔
159
    newControl.push(...stackCopy)
16✔
160
    return newControl
16✔
161
  }
162
}
163

164
/**
165
 * The stash is a list of values that stores intermediate results.
166
 */
167
export class Stash extends Stack<Value> {
79✔
168
  public constructor() {
169
    super()
1,055✔
170
  }
171

172
  public copy(): Stash {
173
    const newStash = new Stash()
16✔
174
    const stackCopy = super.getStack()
16✔
175
    newStash.push(...stackCopy)
16✔
176
    return newStash
16✔
177
  }
178
}
179

180
/**
181
 * The T component is a dictionary of mappings from syntax names to
182
 * their corresponding syntax rule transformers (patterns).
183
 *
184
 * Similar to the E component, there is a matching
185
 * "T" environment tree that is used to store the transformers.
186
 * as such, we need to track the transformers and update them with the environment.
187
 */
188
export class Transformers {
79✔
189
  private parent: Transformers | null
190
  private items: Map<string, Transformer[]>
191
  public constructor(parent?: Transformers) {
192
    this.parent = parent || null
554,552✔
193
    this.items = new Map<string, Transformer[]>()
554,552✔
194
  }
195

196
  // only call this if you are sure that the pattern exists.
197
  public getPattern(name: string): Transformer[] {
198
    // check if the pattern exists in the current transformer
199
    if (this.items.has(name)) {
11✔
200
      return this.items.get(name) as Transformer[]
11✔
201
    }
202
    // else check if the pattern exists in the parent transformer
NEW
203
    if (this.parent) {
×
NEW
204
      return this.parent.getPattern(name)
×
205
    }
206
    // should not get here. use this properly.
NEW
207
    throw new Error(`Pattern ${name} not found in transformers`)
×
208
  }
209

210
  public hasPattern(name: string): boolean {
211
    // check if the pattern exists in the current transformer
212
    if (this.items.has(name)) {
2,636✔
213
      return true
11✔
214
    }
215
    // else check if the pattern exists in the parent transformer
216
    if (this.parent) {
2,625✔
217
      return this.parent.hasPattern(name)
2✔
218
    }
219
    return false
2,623✔
220
  }
221

222
  public addPattern(name: string, item: Transformer[]): void {
223
    this.items.set(name, item)
145✔
224
  }
225
}
226

227
/**
228
 * Function to be called when a program is to be interpreted using
229
 * the explicit control evaluator.
230
 *
231
 * @param program The program to evaluate.
232
 * @param context The context to evaluate the program in.
233
 * @returns The result of running the CSE machine.
234
 */
235
export function evaluate(program: es.Program, context: Context, options: IOptions): Value {
79✔
236
  try {
1,034✔
237
    checkProgramForUndefinedVariables(program, context)
1,034✔
238
  } catch (error) {
239
    context.errors.push(error)
5✔
240
    return new CseError(error)
5✔
241
  }
242
  seq.transform(program)
1,029✔
243

244
  try {
1,029✔
245
    context.runtime.isRunning = true
1,029✔
246
    context.runtime.control = new Control(program)
1,029✔
247
    context.runtime.stash = new Stash()
1,029✔
248
    // set a global transformer if it does not exist.
249
    context.runtime.transformers = context.runtime.transformers
1,029!
250
      ? context.runtime.transformers
251
      : new Transformers()
252

253
    return runCSEMachine(
1,029✔
254
      context,
255
      context.runtime.control,
256
      context.runtime.stash,
257
      options.envSteps,
258
      options.stepLimit,
259
      options.isPrelude
260
    )
261
  } catch (error) {
262
    return new CseError(error)
95✔
263
  } finally {
264
    context.runtime.isRunning = false
1,029✔
265
  }
266
}
267

268
/**
269
 * Function that is called when a user wishes to resume evaluation after
270
 * hitting a breakpoint.
271
 * This should only be called after the first 'evaluate' function has been called so that
272
 * context.runtime.control and context.runtime.stash are defined.
273
 * @param context The context to continue evaluating the program in.
274
 * @returns The result of running the CSE machine.
275
 */
276
export function resumeEvaluate(context: Context) {
79✔
277
  try {
×
278
    context.runtime.isRunning = true
×
279
    return runCSEMachine(context, context.runtime.control!, context.runtime.stash!, -1, -1)
×
280
  } catch (error) {
281
    return new CseError(error)
×
282
  } finally {
283
    context.runtime.isRunning = false
×
284
  }
285
}
286

287
function evaluateImports(program: es.Program, context: Context) {
288
  try {
851✔
289
    const [importNodeMap] = filterImportDeclarations(program)
851✔
290

291
    const environment = currentEnvironment(context)
851✔
292
    for (const [moduleName, nodes] of importNodeMap) {
851✔
293
      const functions = context.nativeStorage.loadedModules[moduleName]
6✔
294
      for (const node of nodes) {
6✔
295
        for (const spec of node.specifiers) {
6✔
296
          declareIdentifier(context, spec.local.name, node, environment)
5✔
297
          let obj: any
298

299
          switch (spec.type) {
5!
300
            case 'ImportSpecifier': {
301
              obj = functions[spec.imported.name]
5✔
302
              break
5✔
303
            }
304
            case 'ImportDefaultSpecifier': {
305
              obj = functions.default
×
306
              break
×
307
            }
308
            case 'ImportNamespaceSpecifier': {
309
              obj = functions
×
310
              break
×
311
            }
312
          }
313

314
          defineVariable(context, spec.local.name, obj, true, node)
5✔
315
        }
316
      }
317
    }
318
  } catch (error) {
319
    handleRuntimeError(context, error)
×
320
  }
321
}
322

323
/**
324
 * Function that returns the appropriate Promise<Result> given the output of CSE machine evaluating, depending
325
 * on whether the program is finished evaluating, ran into a breakpoint or ran into an error.
326
 * @param context The context of the program.
327
 * @param value The value of CSE machine evaluating the program.
328
 * @returns The corresponding promise.
329
 */
330
export function CSEResultPromise(context: Context, value: Value): Promise<Result> {
79✔
331
  return new Promise((resolve, reject) => {
1,034✔
332
    if (value instanceof CSEBreak) {
1,034!
333
      resolve({ status: 'suspended-cse-eval', context })
×
334
    } else if (value instanceof CseError) {
1,034✔
335
      resolve({ status: 'error' })
100✔
336
    } else {
337
      resolve({ status: 'finished', context, value })
934✔
338
    }
339
  })
340
}
341

342
/**
343
 * The primary runner/loop of the explicit control evaluator.
344
 *
345
 * @param context The context to evaluate the program in.
346
 * @param control Points to the current context.runtime.control
347
 * @param stash Points to the current context.runtime.stash
348
 * @param isPrelude Whether the program we are running is the prelude
349
 * @returns A special break object if the program is interrupted by a breakpoint;
350
 * else the top value of the stash. It is usually the return value of the program.
351
 */
352
function runCSEMachine(
353
  context: Context,
354
  control: Control,
355
  stash: Stash,
356
  envSteps: number,
357
  stepLimit: number,
358
  isPrelude: boolean = false
×
359
) {
360
  const eceState = generateCSEMachineStateStream(
1,029✔
361
    context,
362
    control,
363
    stash,
364
    envSteps,
365
    stepLimit,
366
    isPrelude
367
  )
368

369
  // Done intentionally as the state is not needed
370
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
371
  for (const _ of eceState) {
1,029✔
372
  }
373

374
  return stash.peek()
934✔
375
}
376

377
export function* generateCSEMachineStateStream(
79✔
378
  context: Context,
379
  control: Control,
380
  stash: Stash,
381
  envSteps: number,
382
  stepLimit: number,
383
  isPrelude: boolean = false
9✔
384
) {
385
  context.runtime.break = false
1,038✔
386
  context.runtime.nodes = []
1,038✔
387

388
  // steps: number of steps completed
389
  let steps = 0
1,038✔
390

391
  let command = control.peek()
1,038✔
392

393
  // Push first node to be evaluated into context.
394
  // The typeguard is there to guarantee that we are pushing a node (which should always be the case)
395
  if (command !== undefined && isNode(command)) {
1,038✔
396
    context.runtime.nodes.unshift(command)
1,038✔
397
  }
398

399
  while (command !== undefined) {
1,038✔
400
    // Return to capture a snapshot of the control and stash after the target step count is reached
401
    if (!isPrelude && steps === envSteps) {
7,449,728✔
402
      yield { stash, control, steps }
334✔
403
      return
334✔
404
    }
405
    // Step limit reached, stop further evaluation
406
    if (!isPrelude && steps === stepLimit) {
7,449,394!
407
      break
×
408
    }
409

410
    if (isNode(command) && command.type === 'DebuggerStatement') {
7,449,394✔
411
      // steps += 1
412

413
      // Record debugger step if running for the first time
414
      if (envSteps === -1) {
41✔
415
        context.runtime.breakpointSteps.push(steps)
41✔
416
      }
417
    }
418

419
    if (!isPrelude && envChanging(command)) {
7,449,394✔
420
      // command is evaluated on the next step
421
      // Hence, next step will change the environment
422
      context.runtime.changepointSteps.push(steps + 1)
573,861✔
423
    }
424

425
    control.pop()
7,449,394✔
426
    if (isNode(command)) {
7,449,394✔
427
      context.runtime.nodes.shift()
5,410,662✔
428
      context.runtime.nodes.unshift(command)
5,410,662✔
429
      checkEditorBreakpoints(context, command)
5,410,662✔
430
      cmdEvaluators[command.type](command, context, control, stash, isPrelude)
5,410,662✔
431
      if (context.runtime.break && context.runtime.debuggerOn) {
5,410,659✔
432
        // We can put this under isNode since context.runtime.break
433
        // will only be updated after a debugger statement and so we will
434
        // run into a node immediately after.
435
        // With the new evaluator, we don't return a break
436
        // return new CSEBreak()
437
      }
438
    } else if (isInstr(command)) {
2,038,732✔
439
      // Command is an instruction
440
      cmdEvaluators[command.instrType](command, context, control, stash, isPrelude)
2,035,838✔
441
    } else {
442
      // this is a scheme value
443
      schemeEval(command, context, control, stash, isPrelude)
2,894✔
444
    }
445

446
    // Push undefined into the stack if both control and stash is empty
447
    if (control.isEmpty() && stash.isEmpty()) {
7,449,299✔
448
      stash.push(undefined)
429✔
449
    }
450
    command = control.peek()
7,449,299✔
451

452
    steps += 1
7,449,299✔
453
    if (!isPrelude) {
7,449,299✔
454
      context.runtime.envStepsTotal = steps
7,159,506✔
455
    }
456

457
    yield { stash, control, steps }
7,449,299✔
458
  }
459
}
460

461
/**
462
 * Dictionary of functions which handle the logic for the response of the three registers of
463
 * the CSE machine to each ControlItem.
464
 */
465
const cmdEvaluators: { [type: string]: CmdEvaluator } = {
79✔
466
  /**
467
   * Statements
468
   */
469

470
  Program: function (
471
    command: es.BlockStatement,
472
    context: Context,
473
    control: Control,
474
    stash: Stash,
475
    isPrelude: boolean
476
  ) {
477
    // After execution of a program, the current environment might be a local one.
478
    // This can cause issues (for example, during execution of consecutive REPL programs)
479
    // This piece of code will reset the current environment to either a global one, a program one or a prelude one.
480
    while (
1,034✔
481
      currentEnvironment(context).name != 'global' &&
1,884✔
482
      currentEnvironment(context).name != 'programEnvironment' &&
483
      currentEnvironment(context).name != 'prelude'
484
    ) {
485
      popEnvironment(context)
×
486
    }
487

488
    // If the program has outer declarations:
489
    // - Create the program environment (if none exists yet), and
490
    // - Declare the functions and variables in the program environment.
491
    if (hasDeclarations(command) || hasImportDeclarations(command)) {
1,034✔
492
      if (currentEnvironment(context).name != 'programEnvironment') {
851✔
493
        const programEnv = createProgramEnvironment(context, isPrelude)
851✔
494
        pushEnvironment(context, programEnv)
851✔
495
      }
496
      const environment = currentEnvironment(context)
851✔
497
      evaluateImports(command as unknown as es.Program, context)
851✔
498
      declareFunctionsAndVariables(context, command, environment)
851✔
499
    }
500

501
    if (command.body.length == 1) {
1,034✔
502
      // If program only consists of one statement, evaluate it immediately
503
      const next = command.body[0]
166✔
504
      cmdEvaluators[next.type](next, context, control, stash, isPrelude)
166✔
505
    } else {
506
      // Push block body as statement sequence
507
      const seq: StatementSequence = ast.statementSequence(command.body, command.loc)
868✔
508
      control.push(seq)
868✔
509
    }
510
  },
511

512
  BlockStatement: function (command: es.BlockStatement, context: Context, control: Control) {
513
    // To restore environment after block ends
514
    // If there is an env instruction on top of the stack, or if there are no declarations
515
    // we do not need to push another one
516
    // The no declarations case is handled at the transform stage, so no blockStatement node without declarations should end up here.
517
    const next = control.peek()
3,910✔
518

519
    // Push ENVIRONMENT instruction if needed - if next control stack item
520
    // exists and is not an environment instruction, OR the control only contains
521
    // environment indepedent items
522
    if (
3,910✔
523
      next &&
9,369✔
524
      !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) &&
5,398✔
525
      !control.canAvoidEnvInstr()
526
    ) {
527
      control.push(
1,888✔
528
        instr.envInstr(
529
          currentEnvironment(context),
530
          context.runtime.transformers as Transformers,
531
          command
532
        )
533
      )
534
    }
535

536
    const environment = createBlockEnvironment(context, 'blockEnvironment')
3,910✔
537
    declareFunctionsAndVariables(context, command, environment)
3,910✔
538
    pushEnvironment(context, environment)
3,910✔
539

540
    // Push block body as statement sequence
541
    const seq: StatementSequence = ast.statementSequence(command.body, command.loc)
3,910✔
542
    control.push(seq)
3,910✔
543
  },
544

545
  StatementSequence: function (
546
    command: StatementSequence,
547
    context: Context,
548
    control: Control,
549
    stash: Stash,
550
    isPrelude: boolean
551
  ) {
552
    if (command.body.length == 1) {
266,678✔
553
      // If sequence only consists of one statement, evaluate it immediately
554
      const next = command.body[0]
261,729✔
555
      cmdEvaluators[next.type](next, context, control, stash, isPrelude)
261,729✔
556
    } else {
557
      // unpack and push individual nodes in body
558
      control.push(...handleSequence(command.body))
4,949✔
559
    }
560
    return
266,678✔
561
  },
562

563
  WhileStatement: function (
564
    command: es.WhileStatement,
565
    context: Context,
566
    control: Control,
567
    stash: Stash
568
  ) {
569
    if (hasBreakStatement(command.body as es.BlockStatement)) {
14✔
570
      control.push(instr.breakMarkerInstr(command))
2✔
571
    }
572
    control.push(instr.whileInstr(command.test, command.body, command))
14✔
573
    control.push(command.test)
14✔
574
    control.push(ast.identifier('undefined', command.loc)) // Return undefined if there is no loop execution
14✔
575
  },
576

577
  ForStatement: function (command: es.ForStatement, context: Context, control: Control) {
578
    // All 3 parts will be defined due to parser rules
579
    const init = command.init!
668✔
580
    const test = command.test!
668✔
581
    const update = command.update!
668✔
582

583
    // Loop control variable present
584
    // Refer to Source §3 specifications https://docs.sourceacademy.org/source_3.pdf
585
    if (init.type === 'VariableDeclaration' && init.kind === 'let') {
668✔
586
      const id = init.declarations[0].id as es.Identifier
337✔
587
      const valueExpression = init.declarations[0].init!
337✔
588

589
      control.push(
337✔
590
        ast.blockStatement(
591
          [
592
            init,
593
            ast.forStatement(
594
              ast.assignmentExpression(id, valueExpression, command.loc),
595
              test,
596
              update,
597
              ast.blockStatement(
598
                [
599
                  ast.variableDeclaration(
600
                    [
601
                      ast.variableDeclarator(
602
                        ast.identifier(`_copy_of_${id.name}`, command.loc),
603
                        ast.identifier(id.name, command.loc),
604
                        command.loc
605
                      )
606
                    ],
607
                    command.loc
608
                  ),
609
                  ast.blockStatement(
610
                    [
611
                      ast.variableDeclaration(
612
                        [
613
                          ast.variableDeclarator(
614
                            ast.identifier(id.name, command.loc),
615
                            ast.identifier(`_copy_of_${id.name}`, command.loc),
616
                            command.loc
617
                          )
618
                        ],
619
                        command.loc
620
                      ),
621
                      command.body
622
                    ],
623
                    command.loc
624
                  )
625
                ],
626
                command.loc
627
              ),
628
              command.loc
629
            )
630
          ],
631
          command.loc
632
        )
633
      )
634
    } else {
635
      if (hasBreakStatement(command.body as es.BlockStatement)) {
331✔
636
        control.push(instr.breakMarkerInstr(command))
3✔
637
      }
638
      control.push(instr.forInstr(init, test, update, command.body, command))
331✔
639
      control.push(test)
331✔
640
      control.push(instr.popInstr(command)) // Pop value from init assignment
331✔
641
      control.push(init)
331✔
642
      control.push(ast.identifier('undefined', command.loc)) // Return undefined if there is no loop execution
331✔
643
    }
644
  },
645

646
  IfStatement: function (
647
    command: es.IfStatement,
648
    context: Context,
649
    control: Control,
650
    stash: Stash
651
  ) {
652
    control.push(...reduceConditional(command))
130,257✔
653
  },
654

655
  ExpressionStatement: function (
656
    command: es.ExpressionStatement,
657
    context: Context,
658
    control: Control,
659
    stash: Stash,
660
    isPrelude: boolean
661
  ) {
662
    // Fast forward to the expression
663
    // If not the next step will look like it's only removing ';'
664
    cmdEvaluators[command.expression.type](command.expression, context, control, stash, isPrelude)
2,034✔
665
  },
666

667
  DebuggerStatement: function (command: es.DebuggerStatement, context: Context) {
668
    context.runtime.break = true
41✔
669
  },
670

671
  VariableDeclaration: function (
672
    command: es.VariableDeclaration,
673
    context: Context,
674
    control: Control
675
  ) {
676
    const declaration: es.VariableDeclarator = command.declarations[0]
24,120✔
677
    const id = declaration.id as es.Identifier
24,120✔
678

679
    // Parser enforces initialisation during variable declaration
680
    const init = declaration.init!
24,120✔
681

682
    control.push(instr.popInstr(command))
24,120✔
683
    control.push(instr.assmtInstr(id.name, command.kind === 'const', true, command))
24,120✔
684
    control.push(init)
24,120✔
685
  },
686

687
  FunctionDeclaration: function (
688
    command: es.FunctionDeclaration,
689
    context: Context,
690
    control: Control
691
  ) {
692
    // Function declaration desugared into constant declaration.
693
    const lambdaExpression: es.ArrowFunctionExpression = ast.blockArrowFunction(
18,870✔
694
      command.params as es.Identifier[],
695
      command.body,
696
      command.loc
697
    )
698
    const lambdaDeclaration: es.VariableDeclaration = ast.constantDeclaration(
18,870✔
699
      command.id!.name,
700
      lambdaExpression,
701
      command.loc
702
    )
703
    control.push(lambdaDeclaration)
18,870✔
704
  },
705

706
  ReturnStatement: function (command: es.ReturnStatement, context: Context, control: Control) {
707
    // Push return argument onto control as well as Reset Instruction to clear to ignore all statements after the return.
708
    const next = control.peek()
130,187✔
709
    if (next && isInstr(next) && next.instrType === InstrType.MARKER) {
130,187✔
710
      control.pop()
130,026✔
711
    } else {
712
      control.push(instr.resetInstr(command))
161✔
713
    }
714
    if (command.argument) {
130,187✔
715
      control.push(command.argument)
130,187✔
716
    }
717
  },
718

719
  ContinueStatement: function (
720
    command: es.ContinueStatement,
721
    context: Context,
722
    control: Control,
723
    stash: Stash
724
  ) {
725
    control.push(instr.contInstr(command))
4✔
726
  },
727

728
  BreakStatement: function (
729
    command: es.BreakStatement,
730
    context: Context,
731
    control: Control,
732
    stash: Stash
733
  ) {
734
    control.push(instr.breakInstr(command))
4✔
735
  },
736

737
  ImportDeclaration: function () {},
738

739
  /**
740
   * Expressions
741
   */
742

743
  Literal: function (command: es.Literal, context: Context, control: Control, stash: Stash) {
744
    stash.push(command.value)
1,399,200✔
745
  },
746

747
  AssignmentExpression: function (
748
    command: es.AssignmentExpression,
749
    context: Context,
750
    control: Control
751
  ) {
752
    if (command.left.type === 'MemberExpression') {
3,599✔
753
      control.push(instr.arrAssmtInstr(command))
22✔
754
      control.push(command.right)
22✔
755
      control.push(command.left.property)
22✔
756
      control.push(command.left.object)
22✔
757
    } else if (command.left.type === 'Identifier') {
3,577✔
758
      const id = command.left
3,577✔
759
      control.push(instr.assmtInstr(id.name, false, false, command))
3,577✔
760
      control.push(command.right)
3,577✔
761
    }
762
  },
763

764
  ArrayExpression: function (command: es.ArrayExpression, context: Context, control: Control) {
765
    const elems = command.elements as ContiguousArrayElements
1,599✔
766
    reverse(elems)
1,599✔
767
    const len = elems.length
1,599✔
768

769
    control.push(instr.arrLitInstr(len, command))
1,599✔
770
    for (const elem of elems) {
1,599✔
771
      control.push(elem)
1,610✔
772
    }
773
  },
774

775
  MemberExpression: function (
776
    command: es.MemberExpression,
777
    context: Context,
778
    control: Control,
779
    stash: Stash
780
  ) {
781
    control.push(instr.arrAccInstr(command))
4✔
782
    control.push(command.property)
4✔
783
    control.push(command.object)
4✔
784
  },
785

786
  ConditionalExpression: function (
787
    command: es.ConditionalExpression,
788
    context: Context,
789
    control: Control,
790
    stash: Stash
791
  ) {
792
    control.push(...reduceConditional(command))
285,205✔
793
  },
794

795
  Identifier: function (command: es.Identifier, context: Context, control: Control, stash: Stash) {
796
    stash.push(getVariable(context, command.name, command))
1,524,194✔
797
  },
798

799
  UnaryExpression: function (command: es.UnaryExpression, context: Context, control: Control) {
800
    control.push(instr.unOpInstr(command.operator, command))
39✔
801
    control.push(command.argument)
39✔
802
  },
803

804
  BinaryExpression: function (command: es.BinaryExpression, context: Context, control: Control) {
805
    control.push(instr.binOpInstr(command.operator, command))
1,248,892✔
806
    control.push(command.right)
1,248,892✔
807
    control.push(command.left)
1,248,892✔
808
  },
809

810
  LogicalExpression: function (command: es.LogicalExpression, context: Context, control: Control) {
811
    if (command.operator === '&&') {
10,009✔
812
      control.push(
9✔
813
        ast.conditionalExpression(command.left, command.right, ast.literal(false), command.loc)
814
      )
815
    } else {
816
      control.push(
10,000✔
817
        ast.conditionalExpression(command.left, ast.literal(true), command.right, command.loc)
818
      )
819
    }
820
  },
821

822
  ArrowFunctionExpression: function (
823
    command: es.ArrowFunctionExpression,
824
    context: Context,
825
    control: Control,
826
    stash: Stash,
827
    isPrelude: boolean
828
  ) {
829
    const closure: Closure = Closure.makeFromArrowFunction(
19,829✔
830
      command,
831
      currentEnvironment(context),
832
      currentTransformers(context),
833
      context,
834
      true,
835
      isPrelude
836
    )
837
    stash.push(closure)
19,829✔
838
  },
839

840
  CallExpression: function (command: es.CallExpression, context: Context, control: Control) {
841
    // Push application instruction, function arguments and function onto control.
842
    control.push(instr.appInstr(command.arguments.length, command))
604,200✔
843
    for (let index = command.arguments.length - 1; index >= 0; index--) {
604,200✔
844
      control.push(command.arguments[index])
697,355✔
845
    }
846
    control.push(command.callee)
604,200✔
847
  },
848

849
  /**
850
   * Instructions
851
   */
852

853
  [InstrType.RESET]: function (command: Instr, context: Context, control: Control, stash: Stash) {
854
    // Keep pushing reset instructions until marker is found.
855
    const cmdNext: ControlItem | undefined = control.pop()
601✔
856
    if (cmdNext && (!isInstr(cmdNext) || cmdNext.instrType !== InstrType.MARKER)) {
601✔
857
      control.push(instr.resetInstr(command.srcNode))
441✔
858
    }
859
  },
860

861
  [InstrType.WHILE]: function (
862
    command: WhileInstr,
863
    context: Context,
864
    control: Control,
865
    stash: Stash
866
  ) {
867
    const test = stash.pop()
58✔
868

869
    // Check if test condition is a boolean
870
    const error = rttc.checkIfStatement(command.srcNode, test, context.chapter)
58✔
871
    if (error) {
58!
872
      handleRuntimeError(context, error)
×
873
    }
874

875
    if (test) {
58✔
876
      control.push(command)
49✔
877
      control.push(command.test)
49✔
878
      if (hasContinueStatement(command.body as es.BlockStatement)) {
49✔
879
        control.push(instr.contMarkerInstr(command.srcNode))
14✔
880
      }
881
      if (!valueProducing(command.body)) {
49✔
882
        // if loop body is not value-producing, insert undefined expression statement
883
        control.push(ast.identifier('undefined', command.body.loc))
1✔
884
      }
885
      control.push(command.body)
49✔
886
      control.push(instr.popInstr(command.srcNode)) // Pop previous body value
49✔
887
    }
888
  },
889

890
  [InstrType.FOR]: function (command: ForInstr, context: Context, control: Control, stash: Stash) {
891
    const test = stash.pop()
1,757✔
892

893
    // Check if test condition is a boolean
894
    const error = rttc.checkIfStatement(command.srcNode, test, context.chapter)
1,757✔
895
    if (error) {
1,757!
896
      handleRuntimeError(context, error)
×
897
    }
898

899
    if (test) {
1,757✔
900
      control.push(command)
1,753✔
901
      control.push(command.test)
1,753✔
902
      control.push(instr.popInstr(command.srcNode)) // Pop value from update
1,753✔
903
      control.push(command.update)
1,753✔
904
      if (hasContinueStatement(command.body as es.BlockStatement)) {
1,753✔
905
        control.push(instr.contMarkerInstr(command.srcNode))
4✔
906
      }
907
      if (!valueProducing(command.body)) {
1,753✔
908
        // if loop body is not value-producing, insert undefined expression statement
909
        control.push(ast.identifier('undefined', command.body.loc))
1✔
910
      }
911
      control.push(command.body)
1,753✔
912
      control.push(instr.popInstr(command.srcNode)) // Pop previous body value
1,753✔
913
    }
914
  },
915

916
  [InstrType.ASSIGNMENT]: function (
917
    command: AssmtInstr,
918
    context: Context,
919
    control: Control,
920
    stash: Stash
921
  ) {
922
    command.declaration
27,558✔
923
      ? defineVariable(
924
          context,
925
          command.symbol,
926
          stash.peek(),
927
          command.constant,
928
          command.srcNode as es.VariableDeclaration
929
        )
930
      : setVariable(
931
          context,
932
          command.symbol,
933
          stash.peek(),
934
          command.srcNode as es.AssignmentExpression
935
        )
936
  },
937

938
  [InstrType.UNARY_OP]: function (
939
    command: UnOpInstr,
940
    context: Context,
941
    control: Control,
942
    stash: Stash
943
  ) {
944
    const argument = stash.pop()
39✔
945
    const error = rttc.checkUnaryExpression(
39✔
946
      command.srcNode,
947
      command.symbol as es.UnaryOperator,
948
      argument,
949
      context.chapter
950
    )
951
    if (error) {
39!
952
      handleRuntimeError(context, error)
×
953
    }
954
    stash.push(evaluateUnaryExpression(command.symbol as es.UnaryOperator, argument))
39✔
955
  },
956

957
  [InstrType.BINARY_OP]: function (
958
    command: BinOpInstr,
959
    context: Context,
960
    control: Control,
961
    stash: Stash
962
  ) {
963
    const right = stash.pop()
848,822✔
964
    const left = stash.pop()
848,822✔
965
    const error = rttc.checkBinaryExpression(
848,822✔
966
      command.srcNode,
967
      command.symbol as es.BinaryOperator,
968
      context.chapter,
969
      left,
970
      right
971
    )
972
    if (error) {
848,822✔
973
      handleRuntimeError(context, error)
2✔
974
    }
975
    stash.push(evaluateBinaryExpression(command.symbol as es.BinaryOperator, left, right))
848,820✔
976
  },
977

978
  [InstrType.POP]: function (command: Instr, context: Context, control: Control, stash: Stash) {
979
    stash.pop()
28,777✔
980
  },
981

982
  [InstrType.APPLICATION]: function (
983
    command: AppInstr,
984
    context: Context,
985
    control: Control,
986
    stash: Stash
987
  ) {
988
    checkStackOverFlow(context, control)
604,202✔
989
    // Get function arguments from the stash
990
    const args: Value[] = []
604,198✔
991
    for (let index = 0; index < command.numOfArgs; index++) {
604,198✔
992
      args.unshift(stash.pop())
697,357✔
993
    }
994

995
    // Get function from the stash
996
    const func: Closure | Function = stash.pop()
604,198✔
997

998
    if (!(func instanceof Closure || func instanceof Function)) {
604,198✔
999
      handleRuntimeError(context, new errors.CallingNonFunctionValue(func, command.srcNode))
22✔
1000
    }
1001

1002
    if (isApply(func)) {
604,176✔
1003
      // Check for number of arguments mismatch error
1004
      checkNumberOfArguments(context, func, args, command.srcNode)
4✔
1005

1006
      // get the procedure from the arguments
1007
      const proc = args[0]
3✔
1008
      // get the last list from the arguments
1009
      // (and it should be a list)
1010
      const last = args[args.length - 1]
3✔
1011
      if (!isList(last)) {
3✔
1012
        handleRuntimeError(
1✔
1013
          context,
1014
          new errors.ExceptionError(new Error('Last argument of apply must be a list'))
1015
        )
1016
      }
1017
      // get the rest of the arguments between the procedure and the last list
1018
      const rest = args.slice(1, args.length - 1)
2✔
1019
      // convert the last list to an array
1020
      const lastAsArray = flattenList(last)
2✔
1021
      // combine the rest and the last list
1022
      const combined = [...rest, ...lastAsArray]
2✔
1023

1024
      // push the items back onto the stash
1025
      stash.push(proc)
2✔
1026
      stash.push(...combined)
2✔
1027

1028
      // prepare a function call for the procedure
1029
      control.push(instr.appInstr(combined.length, command.srcNode))
2✔
1030
      return
2✔
1031
    }
1032

1033
    if (isEval(func)) {
604,172✔
1034
      // Check for number of arguments mismatch error
1035
      checkNumberOfArguments(context, func, args, command.srcNode)
1,156✔
1036

1037
      // get the AST from the arguments
1038
      const AST = args[0]
1,156✔
1039

1040
      // move it to the control
1041
      control.push(AST)
1,156✔
1042
      return
1,156✔
1043
    }
1044

1045
    if (isCallWithCurrentContinuation(func)) {
603,016✔
1046
      // Check for number of arguments mismatch error
1047
      checkNumberOfArguments(context, func, args, command.srcNode)
10✔
1048

1049
      // generate a continuation here
1050
      const contControl = control.copy()
6✔
1051
      const contStash = stash.copy()
6✔
1052
      const contEnv = context.runtime.environments.slice()
6✔
1053
      const contTransformers = currentTransformers(context)
6✔
1054

1055
      // at this point, the extra CALL instruction
1056
      // has been removed from the control stack.
1057
      // additionally, the single closure argument has been
1058
      // removed (as the parameter of call/cc) from the stash
1059
      // and additionally, call/cc itself has been removed from the stash.
1060

1061
      // as such, there is no further need to modify the
1062
      // copied C, S, E and T!
1063

1064
      const continuation = new Continuation(
6✔
1065
        context,
1066
        contControl,
1067
        contStash,
1068
        contEnv,
1069
        contTransformers
1070
      )
1071

1072
      // Get the callee
1073
      const cont_callee: Value = args[0]
6✔
1074

1075
      const dummyFCallExpression = makeDummyContCallExpression('f', 'cont')
6✔
1076

1077
      // Prepare a function call for the continuation-consuming function
1078
      control.push(instr.appInstr(command.numOfArgs, dummyFCallExpression))
6✔
1079

1080
      // push the argument (the continuation caller) back onto the stash
1081
      stash.push(cont_callee)
6✔
1082

1083
      // finally, push the continuation onto the stash
1084
      stash.push(continuation)
6✔
1085
      return
6✔
1086
    }
1087

1088
    if (func instanceof Continuation) {
603,006✔
1089
      // Check for number of arguments mismatch error
1090
      checkNumberOfArguments(context, func, args, command.srcNode)
3✔
1091

1092
      // get the C, S, E from the continuation
1093
      const contControl = func.getControl()
3✔
1094
      const contStash = func.getStash()
3✔
1095
      const contEnv = func.getEnv()
3✔
1096
      const contTransformers = func.getTransformers()
3✔
1097

1098
      // update the C, S, E of the current context
1099
      control.setTo(contControl)
3✔
1100
      stash.setTo(contStash)
3✔
1101
      context.runtime.environments = contEnv
3✔
1102
      setTransformers(context, contTransformers)
3✔
1103

1104
      // push the arguments back onto the stash
1105
      stash.push(...args)
3✔
1106
      return
3✔
1107
    }
1108

1109
    if (func instanceof Closure) {
603,003✔
1110
      // Check for number of arguments mismatch error
1111
      checkNumberOfArguments(context, func, args, command.srcNode)
550,538✔
1112

1113
      const next = control.peek()
550,525✔
1114

1115
      // Push ENVIRONMENT instruction if needed - if next control stack item
1116
      // exists and is not an environment instruction, OR the control only contains
1117
      // environment indepedent items
1118
      // if the current language is a scheme language, don't avoid the environment instruction
1119
      // as schemers like using the REPL, and that always assumes that the environment is reset
1120
      // to the main environment.
1121
      if (
550,525✔
1122
        (next &&
2,056,519✔
1123
          !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) &&
1,060,818✔
1124
          !control.canAvoidEnvInstr()) ||
1125
        isSchemeLanguage(context)
1126
      ) {
1127
        control.push(
105,320✔
1128
          instr.envInstr(currentEnvironment(context), currentTransformers(context), command.srcNode)
1129
        )
1130
      }
1131

1132
      // Create environment for function parameters if the function isn't nullary.
1133
      // Name the environment if the function call expression is not anonymous
1134
      if (args.length > 0) {
550,525✔
1135
        const environment = createEnvironment(context, func, args, command.srcNode)
450,423✔
1136
        pushEnvironment(context, environment)
450,423✔
1137
      } else {
1138
        context.runtime.environments.unshift(func.environment)
100,102✔
1139
      }
1140

1141
      // Handle special case if function is simple
1142
      if (isSimpleFunction(func.node)) {
550,525✔
1143
        // Closures convert ArrowExpressionStatements to BlockStatements
1144
        const block = func.node.body as es.BlockStatement
420,333✔
1145
        const returnStatement = block.body[0] as es.ReturnStatement
420,333✔
1146
        control.push(returnStatement.argument ?? ast.identifier('undefined', returnStatement.loc))
420,333!
1147
      } else {
1148
        if (control.peek()) {
130,192✔
1149
          // push marker if control not empty
1150
          control.push(instr.markerInstr(command.srcNode))
130,161✔
1151
        }
1152
        control.push(func.node.body)
130,192✔
1153
      }
1154

1155
      // we need to update the transformers environment here.
1156
      const newTransformers = new Transformers(func.transformers)
550,525✔
1157
      setTransformers(context, newTransformers)
550,525✔
1158
      return
550,525✔
1159
    }
1160

1161
    // Value is a built-in function
1162
    // Check for number of arguments mismatch error
1163
    checkNumberOfArguments(context, func, args, command.srcNode)
52,465✔
1164
    // Directly stash result of applying pre-built functions without the CSE machine.
1165
    try {
52,461✔
1166
      const result = func(...args)
52,461✔
1167

1168
      if (isStreamFn(func, result)) {
52,453✔
1169
        // This is a special case for the `stream` built-in function, since it returns pairs
1170
        // whose last element is a function. The CSE machine on the frontend will still draw
1171
        // these functions like closures, and the tail of the "closures" will need to point
1172
        // to where `stream` was called.
1173
        //
1174
        // TODO: remove this condition if `stream` becomes a pre-defined function
1175
        Object.defineProperties(result[1], {
19✔
1176
          environment: { value: currentEnvironment(context), writable: true }
1177
        })
1178
      }
1179

1180
      // Recursively adds `environment` and `id` properties to any arrays created,
1181
      // and also adds them to the heap starting from the arrays that are more deeply nested.
1182
      const attachEnvToResult = (value: any) => {
52,453✔
1183
        // Built-in functions don't instantly create arrays with circular references, so
1184
        // there is no need to keep track of visited arrays.
1185
        if (isArray(value) && !isEnvArray(value)) {
152,859✔
1186
          for (const item of value) {
50,203✔
1187
            attachEnvToResult(item)
100,406✔
1188
          }
1189
          handleArrayCreation(context, value)
50,203✔
1190
        }
1191
      }
1192
      attachEnvToResult(result)
52,453✔
1193

1194
      stash.push(result)
52,453✔
1195
    } catch (error) {
1196
      if (!(error instanceof RuntimeSourceError || error instanceof errors.ExceptionError)) {
8✔
1197
        // The error could've arisen when the builtin called a source function which errored.
1198
        // If the cause was a source error, we don't want to include the error.
1199
        // However if the error came from the builtin itself, we need to handle it.
1200
        const loc = command.srcNode.loc ?? UNKNOWN_LOCATION
8!
1201
        handleRuntimeError(context, new errors.ExceptionError(error, loc))
8✔
1202
      }
1203
    }
1204
  },
1205

1206
  [InstrType.BRANCH]: function (
1207
    command: BranchInstr,
1208
    context: Context,
1209
    control: Control,
1210
    stash: Stash
1211
  ) {
1212
    const test = stash.pop()
415,462✔
1213

1214
    // Check if test condition is a boolean
1215
    const error = rttc.checkIfStatement(command.srcNode, test, context.chapter)
415,462✔
1216
    if (error) {
415,462✔
1217
      handleRuntimeError(context, error)
1✔
1218
    }
1219

1220
    if (test) {
415,461✔
1221
      if (!valueProducing(command.consequent)) {
10,290✔
1222
        control.push(ast.identifier('undefined', command.consequent.loc))
6✔
1223
      }
1224
      control.push(command.consequent)
10,290✔
1225
    } else if (command.alternate) {
405,171✔
1226
      if (!valueProducing(command.alternate)) {
405,139✔
1227
        control.push(ast.identifier('undefined', command.consequent.loc))
5✔
1228
      }
1229
      control.push(command.alternate)
405,139✔
1230
    } else {
1231
      control.push(ast.identifier('undefined', command.srcNode.loc))
32✔
1232
    }
1233
  },
1234

1235
  [InstrType.ENVIRONMENT]: function (command: EnvInstr, context: Context) {
1236
    // Restore environment
1237
    while (currentEnvironment(context).id !== command.env.id) {
106,902✔
1238
      popEnvironment(context)
108,717✔
1239
    }
1240
    // restore transformers environment
1241
    setTransformers(context, command.transformers)
106,902✔
1242
  },
1243

1244
  [InstrType.ARRAY_LITERAL]: function (
1245
    command: ArrLitInstr,
1246
    context: Context,
1247
    control: Control,
1248
    stash: Stash
1249
  ) {
1250
    const arity = command.arity
1,579✔
1251
    const array = []
1,579✔
1252
    for (let i = 0; i < arity; ++i) {
1,579✔
1253
      array.unshift(stash.pop())
1,590✔
1254
    }
1255
    handleArrayCreation(context, array)
1,579✔
1256
    stash.push(array)
1,579✔
1257
  },
1258

1259
  [InstrType.ARRAY_ACCESS]: function (
1260
    command: Instr,
1261
    context: Context,
1262
    control: Control,
1263
    stash: Stash
1264
  ) {
1265
    const index = stash.pop()
4✔
1266
    const array = stash.pop()
4✔
1267
    stash.push(array[index])
4✔
1268
  },
1269

1270
  [InstrType.ARRAY_ASSIGNMENT]: function (
1271
    command: Instr,
1272
    context: Context,
1273
    control: Control,
1274
    stash: Stash
1275
  ) {
1276
    const value = stash.pop()
22✔
1277
    const index = stash.pop()
22✔
1278
    const array = stash.pop()
22✔
1279
    array[index] = value
22✔
1280
    stash.push(value)
22✔
1281
  },
1282

1283
  [InstrType.CONTINUE]: function (
1284
    command: Instr,
1285
    context: Context,
1286
    control: Control,
1287
    stash: Stash
1288
  ) {
1289
    const next = control.pop() as ControlItem
17✔
1290
    if (isInstr(next) && next.instrType == InstrType.CONTINUE_MARKER) {
17✔
1291
      // Encountered continue mark, stop popping
1292
    } else if (isInstr(next) && next.instrType == InstrType.ENVIRONMENT) {
13✔
1293
      control.push(command)
3✔
1294
      control.push(next) // Let instruction evaluate to restore env
3✔
1295
    } else {
1296
      // Continue popping from control by pushing same instruction on control
1297
      control.push(command)
10✔
1298
    }
1299
  },
1300

1301
  [InstrType.CONTINUE_MARKER]: function () {},
1302

1303
  [InstrType.BREAK]: function (command: Instr, context: Context, control: Control, stash: Stash) {
1304
    const next = control.pop() as ControlItem
27✔
1305
    if (isInstr(next) && next.instrType == InstrType.BREAK_MARKER) {
27✔
1306
      // Encountered break mark, stop popping
1307
    } else if (isInstr(next) && next.instrType == InstrType.ENVIRONMENT) {
23✔
1308
      control.push(command)
4✔
1309
      control.push(next) // Let instruction evaluate to restore env
4✔
1310
    } else {
1311
      // Continue popping from control by pushing same instruction on control
1312
      control.push(command)
19✔
1313
    }
1314
  },
1315

1316
  [InstrType.BREAK_MARKER]: function () {}
1317
}
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