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

source-academy / js-slang / 5936042370

22 Aug 2023 07:58AM UTC coverage: 83.014% (+0.1%) from 82.893%
5936042370

Pull #1471

github

web-flow
Merge 6d88e2972 into 1d991166b
Pull Request #1471: Use Asynchronous Code for Loading Modules

3618 of 4779 branches covered (75.71%)

Branch coverage included in aggregate %.

269 of 269 new or added lines in 23 files covered. (100.0%)

10838 of 12635 relevant lines covered (85.78%)

106101.79 hits per line

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

64.54
/src/stepper/stepper.ts
1
import { generate } from 'astring'
67✔
2
import type * as es from 'estree'
3
import { partition } from 'lodash'
67✔
4

5
import { type IOptions } from '..'
6
import * as errors from '../errors/errors'
67✔
7
import { UndefinedImportError } from '../modules/errors'
67✔
8
import { initModuleContextAsync, loadModuleBundleAsync } from '../modules/moduleLoaderAsync'
67✔
9
import type { ImportTransformOptions } from '../modules/moduleTypes'
10
import { parse } from '../parser/parser'
67✔
11
import {
12
  BlockExpression,
13
  Context,
14
  ContiguousArrayElementExpression,
15
  ContiguousArrayElements,
16
  FunctionDeclarationExpression,
17
  substituterNodes
18
} from '../types'
19
import assert from '../utils/assert'
67✔
20
import { isImportDeclaration } from '../utils/ast/typeGuards'
67✔
21
import * as ast from '../utils/astCreator'
67✔
22
import {
67✔
23
  dummyBlockExpression,
24
  dummyBlockStatement,
25
  dummyExpression,
26
  dummyProgram,
27
  dummyStatement,
28
  dummyVariableDeclarator
29
} from '../utils/dummyAstCreator'
30
import { evaluateBinaryExpression, evaluateUnaryExpression } from '../utils/operators'
67✔
31
import * as rttc from '../utils/rttc'
67✔
32
import { nodeToValue, objectToString, valueToExpression } from './converter'
67✔
33
import * as builtin from './lib'
67✔
34
import {
67✔
35
  currentEnvironment,
36
  declareIdentifier,
37
  defineVariable,
38
  getDeclaredNames,
39
  handleRuntimeError,
40
  isAllowedLiterals,
41
  isBuiltinFunction,
42
  isImportedFunction,
43
  isNegNumber
44
} from './util'
45

46
const irreducibleTypes = new Set<string>([
67✔
47
  'Literal',
48
  'FunctionExpression',
49
  'ArrowFunctionExpression',
50
  'ArrayExpression'
51
])
52

53
function isIrreducible(node: substituterNodes, context: Context) {
54
  return (
54,976✔
55
    isBuiltinFunction(node) ||
274,113✔
56
    isImportedFunction(node, context) ||
57
    isAllowedLiterals(node) ||
58
    isNegNumber(node) ||
59
    irreducibleTypes.has(node.type)
60
  )
61
}
62

63
type irreducibleNodes =
64
  | es.FunctionExpression
65
  | es.ArrowFunctionExpression
66
  | es.Literal
67
  | es.ArrayExpression
68

69
function scanOutBoundNames(
70
  node: es.BlockStatement | BlockExpression | es.Expression
71
): es.Identifier[] {
72
  const declaredIds: es.Identifier[] = []
13,477✔
73
  if (node.type == 'ArrowFunctionExpression') {
13,477✔
74
    for (const param of node.params) {
96✔
75
      declaredIds.push(param as es.Identifier)
96✔
76
    }
77
  } else if (node.type == 'BlockExpression' || node.type == 'BlockStatement') {
13,381✔
78
    for (const stmt of node.body) {
11,949✔
79
      // if stmt is assignment or functionDeclaration
80
      // add stmt into a set of identifiers
81
      // return that set
82
      if (stmt.type === 'VariableDeclaration') {
14,367✔
83
        stmt.declarations
2,007✔
84
          .map(decn => (decn as es.VariableDeclarator).id as es.Identifier)
2,007✔
85
          .forEach(name => declaredIds.push(name))
2,007✔
86
        for (const decn of stmt.declarations) {
2,007✔
87
          if (
2,007✔
88
            decn.init !== null &&
6,021✔
89
            decn.init !== undefined &&
90
            decn.init.type == 'ArrowFunctionExpression'
91
          ) {
92
            for (const param of decn.init.params) {
15✔
93
              declaredIds.push(param as es.Identifier)
15✔
94
            }
95
          }
96
        }
97
      } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
12,360✔
98
        declaredIds.push(stmt.id)
444✔
99
        stmt.params.forEach(param => declaredIds.push(param as es.Identifier))
512✔
100
      }
101
    }
102
  }
103
  return declaredIds
13,477✔
104
}
105

106
function scanOutDeclarations(
107
  node: es.BlockStatement | BlockExpression | es.Expression | es.Program
108
): es.Identifier[] {
109
  const declaredIds: es.Identifier[] = []
13,033✔
110
  if (
13,033✔
111
    node.type === 'BlockExpression' ||
26,138✔
112
    node.type === 'BlockStatement' ||
113
    node.type === 'Program'
114
  ) {
115
    for (const stmt of node.body) {
12,961✔
116
      // if stmt is assignment or functionDeclaration
117
      // add stmt into a set of identifiers
118
      // return that set
119
      if (stmt.type === 'VariableDeclaration') {
16,120✔
120
        stmt.declarations
2,585✔
121
          .map(decn => (decn as es.VariableDeclarator).id as es.Identifier)
2,585✔
122
          .forEach(name => declaredIds.push(name))
2,585✔
123
      } else if (stmt.type === 'FunctionDeclaration' && stmt.id) {
13,535✔
124
        declaredIds.push(stmt.id)
388✔
125
      }
126
    }
127
  }
128
  return declaredIds
13,033✔
129
}
130

131
function getFreshName(
132
  paramName: string,
133
  counter: number,
134
  freeTarget: string[],
135
  freeReplacement: string[],
136
  boundTarget: es.Identifier[],
137
  boundUpperScope: string[],
138
  boundReplacement: es.Identifier[]
139
): string {
140
  let added = true
37✔
141
  while (added) {
37✔
142
    added = false
74✔
143
    for (const f of freeTarget) {
74✔
144
      if (paramName + '_' + counter === f) {
107✔
145
        counter++
14✔
146
        added = true
14✔
147
      }
148
    }
149
    for (const free of freeReplacement) {
74✔
150
      if (paramName + '_' + counter === free) {
131✔
151
        counter++
12✔
152
        added = true
12✔
153
      }
154
    }
155
    for (const notFree of boundTarget) {
74✔
156
      if (paramName + '_' + counter === notFree.name) {
65✔
157
        counter++
3✔
158
        added = true
3✔
159
      }
160
    }
161
    for (const boundName of boundUpperScope) {
74✔
162
      if (paramName + '_' + counter === boundName) {
31✔
163
        counter++
11✔
164
        added = true
11✔
165
      }
166
    }
167
    for (const identifier of boundReplacement) {
74✔
168
      if (paramName + '_' + counter === identifier.name) {
15✔
169
        counter++
6✔
170
        added = true
6✔
171
      }
172
    }
173
  }
174
  return paramName + '_' + counter
37✔
175
}
176

177
function findMain(
178
  target:
179
    | es.FunctionExpression
180
    | es.ArrowFunctionExpression
181
    | es.BlockStatement
182
    | BlockExpression
183
    | es.FunctionDeclaration
184
    | es.Program,
185
  seenBefore: Map<substituterNodes, substituterNodes>
186
): string[] {
187
  const params: string[] = []
32,026✔
188
  if (
32,026✔
189
    target.type == 'FunctionExpression' ||
61,030✔
190
    target.type == 'ArrowFunctionExpression' ||
191
    target.type === 'FunctionDeclaration'
192
  ) {
193
    if (target.type == 'FunctionExpression' || target.type === 'FunctionDeclaration') {
25,525✔
194
      params.push(target.id!.name)
18,929✔
195
    }
196
    for (let i = 0; i < target.params.length; i++) {
25,525✔
197
      params.push((target.params[i] as es.Identifier).name)
44,175✔
198
    }
199
  }
200

201
  const freeNames: any[] = []
32,026✔
202

203
  const finders = {
32,026✔
204
    Identifier(target: es.Identifier): void {
205
      seenBefore.set(target, target)
168,109✔
206
      let bound = false
168,109✔
207
      for (let i = 0; i < params.length; i++) {
168,109✔
208
        if (target.name == params[i]) {
482,937✔
209
          bound = true
106,801✔
210
          break
106,801✔
211
        }
212
      }
213
      if (!bound) {
168,109✔
214
        freeNames.push(target.name)
61,308✔
215
      }
216
    },
217

218
    ExpressionStatement(target: es.ExpressionStatement): void {
219
      seenBefore.set(target, target)
627✔
220
      find(target.expression)
627✔
221
    },
222

223
    BinaryExpression(target: es.BinaryExpression): void {
224
      seenBefore.set(target, target)
14,495✔
225
      find(target.left)
14,495✔
226
      find(target.right)
14,495✔
227
    },
228

229
    UnaryExpression(target: es.UnaryExpression): void {
230
      seenBefore.set(target, target)
1,152✔
231
      find(target.argument)
1,152✔
232
    },
233

234
    ConditionalExpression(target: es.ConditionalExpression): void {
235
      seenBefore.set(target, target)
15,919✔
236
      find(target.test)
15,919✔
237
      find(target.consequent)
15,919✔
238
      find(target.alternate)
15,919✔
239
    },
240

241
    LogicalExpression(target: es.LogicalExpression): void {
242
      seenBefore.set(target, target)
5,525✔
243
      find(target.left)
5,525✔
244
      find(target.right)
5,525✔
245
    },
246

247
    CallExpression(target: es.CallExpression): void {
248
      seenBefore.set(target, target)
83,826✔
249
      for (let i = 0; i < target.arguments.length; i++) {
83,826✔
250
        find(target.arguments[i])
121,735✔
251
      }
252
      find(target.callee)
83,826✔
253
    },
254

255
    FunctionDeclaration(target: es.FunctionDeclaration): void {
256
      seenBefore.set(target, target)
562✔
257
      const freeInNested = findMain(target, seenBefore)
562✔
258
      for (const free of freeInNested) {
562✔
259
        let bound = false
1,249✔
260
        for (const param of params) {
1,249✔
261
          if (free === param) {
4,473✔
262
            bound = true
387✔
263
          }
264
        }
265
        if (!bound) {
1,249✔
266
          freeNames.push(free)
862✔
267
        }
268
      }
269
    },
270

271
    ArrowFunctionExpression(target: es.ArrowFunctionExpression): void {
272
      seenBefore.set(target, target)
4,923✔
273
      const freeInNested = findMain(target, seenBefore)
4,923✔
274
      for (const free of freeInNested) {
4,923✔
275
        let bound = false
9,788✔
276
        for (const param of params) {
9,788✔
277
          if (free === param) {
31,744✔
278
            bound = true
6,084✔
279
          }
280
        }
281
        if (!bound) {
9,788✔
282
          freeNames.push(free)
3,704✔
283
        }
284
      }
285
    },
286

287
    Program(target: es.Program): void {
288
      seenBefore.set(target, target)
×
289
      target.body.forEach(stmt => {
×
290
        find(stmt)
×
291
      })
292
    },
293

294
    BlockStatement(target: es.BlockStatement): void {
295
      seenBefore.set(target, target)
22,891✔
296
      const declaredNames: Set<string> = getDeclaredNames(target)
22,891✔
297
      for (const item of declaredNames.values()) {
22,891✔
298
        params.push(item)
5,059✔
299
      }
300
      target.body.forEach(stmt => {
22,891✔
301
        find(stmt)
28,520✔
302
      })
303
    },
304

305
    BlockExpression(target: BlockExpression): void {
306
      seenBefore.set(target, target)
×
307
      const declaredNames: Set<string> = getDeclaredNames(target)
×
308
      for (const item of declaredNames.values()) {
×
309
        params.push(item)
×
310
      }
311
      target.body.forEach(stmt => {
×
312
        find(stmt)
×
313
      })
314
    },
315

316
    ReturnStatement(target: es.ReturnStatement): void {
317
      seenBefore.set(target, target)
20,875✔
318
      find(target.argument!)
20,875✔
319
    },
320

321
    VariableDeclaration(target: es.VariableDeclaration): void {
322
      seenBefore.set(target, target)
4,497✔
323
      target.declarations.forEach(dec => {
4,497✔
324
        find(dec)
4,497✔
325
      })
326
    },
327

328
    VariableDeclarator(target: es.VariableDeclarator): void {
329
      seenBefore.set(target, target)
4,497✔
330
      find(target.init!)
4,497✔
331
    },
332

333
    IfStatement(target: es.IfStatement): void {
334
      seenBefore.set(target, target)
1,933✔
335
      find(target.test)
1,933✔
336
      find(target.consequent)
1,933✔
337
      find(target.alternate)
1,933✔
338
    },
339

340
    ArrayExpression(target: es.ArrayExpression): void {
341
      seenBefore.set(target, target)
1,192✔
342
      target.elements.forEach(ele => {
1,192✔
343
        find(ele)
2,384✔
344
      })
345
    }
346
  }
347

348
  function find(target: any): void {
349
    const result = seenBefore.get(target)
393,735✔
350
    if (!result) {
393,735✔
351
      const finder = finders[target.type]
390,881✔
352
      if (finder === undefined) {
390,881✔
353
        seenBefore.set(target, target)
39,858✔
354
      } else {
355
        return finder(target)
351,023✔
356
      }
357
    }
358
  }
359
  find(target.body)
32,026✔
360
  return freeNames
32,026✔
361
}
362

363
/* tslint:disable:no-shadowed-variable */
364
// wrapper function, calls substitute immediately.
365
function substituteMain(
366
  name: es.Identifier,
367
  replacement: irreducibleNodes | es.Identifier,
368
  target: substituterNodes,
369
  paths: string[][]
370
): [substituterNodes, string[][]] {
371
  const seenBefore: Map<substituterNodes, substituterNodes> = new Map()
1,503✔
372

373
  // initialises array to keep track of all paths visited
374
  // without modifying input path array
375
  const allPaths: string[][] = []
1,503✔
376
  let allPathsIndex = 0
1,503✔
377
  const endMarker = '$'
1,503✔
378

379
  if (paths[0] === undefined) {
1,503!
380
    allPaths.push([])
×
381
  } else {
382
    allPaths.push([...paths[0]])
1,503✔
383
  }
384

385
  // substituters will stop expanding the path if index === -1
386
  const pathNotEnded = (index: number) => index > -1
100,637✔
387

388
  // branches out path into two different paths,
389
  // returns array index of branched path
390
  function branch(index: number): number {
391
    allPathsIndex++
49,976✔
392
    allPaths[allPathsIndex] = [...allPaths[index]]
49,976✔
393
    return allPathsIndex
49,976✔
394
  }
395

396
  // keeps track of names in upper scope so that it doesnt rename to these names
397
  const boundUpperScope: string[] = []
1,503✔
398

399
  /**
400
   * Substituters are invoked only when the target is not seen before,
401
   *  therefore each function has the responsbility of registering the
402
   *  [target, replacement] pair in seenBefore.
403
   * How substituters work:
404
   * 1. Create dummy replacement and push [target, dummyReplacement]
405
   *    into the seenBefore array.
406
   * 2. [Recursive step] substitute the children;
407
   *    for each child, branch out the current path
408
   *    and push the appropriate access string into the path
409
   * 3. Return the dummyReplacement
410
   */
411
  const substituters = {
1,503✔
412
    // if name to be replaced is found,
413
    // push endMarker into path
414
    Identifier(
415
      target: es.Identifier,
416
      index: number
417
    ): es.Identifier | FunctionDeclarationExpression | es.Literal | es.Expression {
418
      const re = / rename$/
45,302✔
419
      if (replacement.type === 'Literal') {
45,302✔
420
        // only accept string, boolean and numbers for arguments
421
        if (target.name === name.name) {
3,139✔
422
          if (pathNotEnded(index)) {
545✔
423
            allPaths[index].push(endMarker)
545✔
424
          }
425
          return ast.primitive(replacement.value)
545✔
426
        } else {
427
          return target
2,594✔
428
        }
429
      } else if (replacement.type === 'Identifier' && re.test(replacement.name)) {
42,163✔
430
        if (target.name === name.name) {
5✔
431
          if (pathNotEnded(index)) {
1✔
432
            allPaths[index].push(endMarker)
1✔
433
          }
434
          return ast.identifier(replacement.name.split(' ')[0], replacement.loc)
1✔
435
        } else {
436
          return target
4✔
437
        }
438
      } else {
439
        if (target.name === name.name) {
42,158✔
440
          if (pathNotEnded(index)) {
625✔
441
            allPaths[index].push(endMarker)
624✔
442
          }
443
          return substitute(replacement, -1) as FunctionDeclarationExpression
625✔
444
        } else {
445
          return target
41,533✔
446
        }
447
      }
448
    },
449

450
    ExpressionStatement(target: es.ExpressionStatement, index: number): es.ExpressionStatement {
451
      const substedExpressionStatement = ast.expressionStatement(dummyExpression())
1,174✔
452
      seenBefore.set(target, substedExpressionStatement)
1,174✔
453
      if (pathNotEnded(index)) {
1,174✔
454
        allPaths[index].push('expression')
1,172✔
455
      }
456
      substedExpressionStatement.expression = substitute(target.expression, index) as es.Expression
1,174✔
457
      return substedExpressionStatement
1,174✔
458
    },
459

460
    BinaryExpression(target: es.BinaryExpression, index: number): es.BinaryExpression {
461
      const substedBinaryExpression = ast.binaryExpression(
4,567✔
462
        target.operator,
463
        dummyExpression(),
464
        dummyExpression(),
465
        target.loc
466
      )
467
      seenBefore.set(target, substedBinaryExpression)
4,567✔
468
      let nextIndex = index
4,567✔
469
      if (pathNotEnded(index)) {
4,567✔
470
        nextIndex = branch(index)
4,382✔
471
        allPaths[index].push('left')
4,382✔
472
        allPaths[nextIndex].push('right')
4,382✔
473
      }
474
      substedBinaryExpression.left = substitute(target.left, index) as es.Expression
4,567✔
475
      substedBinaryExpression.right = substitute(target.right, nextIndex) as es.Expression
4,567✔
476
      return substedBinaryExpression
4,567✔
477
    },
478

479
    UnaryExpression(target: es.UnaryExpression, index: number): es.UnaryExpression {
480
      const substedUnaryExpression = ast.unaryExpression(
63✔
481
        target.operator,
482
        dummyExpression(),
483
        target.loc
484
      )
485
      seenBefore.set(target, substedUnaryExpression)
63✔
486
      if (pathNotEnded(index)) {
63✔
487
        allPaths[index].push('argument')
60✔
488
      }
489
      substedUnaryExpression.argument = substitute(target.argument, index) as es.Expression
63✔
490
      return substedUnaryExpression
63✔
491
    },
492

493
    ConditionalExpression(
494
      target: es.ConditionalExpression,
495
      index: number
496
    ): es.ConditionalExpression {
497
      const substedConditionalExpression = ast.conditionalExpression(
3,519✔
498
        dummyExpression(),
499
        dummyExpression(),
500
        dummyExpression(),
501
        target.loc
502
      )
503
      seenBefore.set(target, substedConditionalExpression)
3,519✔
504
      let nextIndex = index
3,519✔
505
      let thirdIndex = index
3,519✔
506
      if (pathNotEnded(index)) {
3,519✔
507
        nextIndex = branch(index)
3,346✔
508
        thirdIndex = branch(index)
3,346✔
509
        allPaths[index].push('test')
3,346✔
510
        allPaths[nextIndex].push('consequent')
3,346✔
511
        allPaths[thirdIndex].push('alternate')
3,346✔
512
      }
513
      substedConditionalExpression.test = substitute(target.test, index) as es.Expression
3,519✔
514
      substedConditionalExpression.consequent = substitute(
3,519✔
515
        target.consequent,
516
        nextIndex
517
      ) as es.Expression
518
      substedConditionalExpression.alternate = substitute(
3,519✔
519
        target.alternate,
520
        thirdIndex
521
      ) as es.Expression
522
      return substedConditionalExpression
3,519✔
523
    },
524

525
    LogicalExpression(target: es.LogicalExpression, index: number): es.LogicalExpression {
526
      const substedLocialExpression = ast.logicalExpression(
218✔
527
        target.operator,
528
        target.left,
529
        target.right
530
      )
531
      seenBefore.set(target, substedLocialExpression)
218✔
532
      let nextIndex = index
218✔
533
      if (pathNotEnded(index)) {
218✔
534
        nextIndex = branch(index)
217✔
535
        allPaths[index].push('left')
217✔
536
        allPaths[nextIndex].push('right')
217✔
537
      }
538
      substedLocialExpression.left = substitute(target.left, index) as es.Expression
218✔
539
      substedLocialExpression.right = substitute(target.right, nextIndex) as es.Expression
218✔
540
      return substedLocialExpression
218✔
541
    },
542

543
    CallExpression(target: es.CallExpression, index: number): es.CallExpression {
544
      const dummyArgs = target.arguments.map(() => dummyExpression())
31,852✔
545
      const substedCallExpression = ast.callExpression(dummyExpression(), dummyArgs, target.loc)
21,722✔
546
      seenBefore.set(target, substedCallExpression)
21,722✔
547
      const arr: number[] = []
21,722✔
548
      let nextIndex = index
21,722✔
549
      for (let i = 0; i < target.arguments.length; i++) {
21,722✔
550
        if (pathNotEnded(index)) {
31,852✔
551
          nextIndex = branch(index)
30,200✔
552
          allPaths[nextIndex].push('arguments[' + i + ']')
30,200✔
553
        }
554
        arr[i] = nextIndex
31,852✔
555
        dummyArgs[i] = substitute(target.arguments[i], nextIndex) as es.Expression
31,852✔
556
      }
557
      if (pathNotEnded(index)) {
21,722✔
558
        allPaths[index].push('callee')
20,563✔
559
      }
560
      substedCallExpression.callee = substitute(target.callee, index) as es.Expression
21,722✔
561
      return substedCallExpression
21,722✔
562
    },
563

564
    FunctionDeclaration(target: es.FunctionDeclaration, index: number): es.FunctionDeclaration {
565
      const substedParams: es.Identifier[] = []
4,141✔
566
      // creates a copy of the params so that the renaming only happens during substitution.
567
      for (let i = 0; i < target.params.length; i++) {
4,141✔
568
        const param = target.params[i] as es.Identifier
8,135✔
569
        substedParams.push(ast.identifier(param.name, param.loc))
8,135✔
570
      }
571
      const re = / rename$/
4,141✔
572
      let newID: es.Identifier
573
      let newBody = target.body
4,141✔
574
      if (replacement.type === 'Identifier' && re.test(replacement.name)) {
4,141✔
575
        // renaming function name
576
        newID = ast.identifier(replacement.name.split(' ')[0], replacement.loc)
1✔
577
      } else {
578
        newID = ast.identifier((target.id as es.Identifier).name, target.loc)
4,140✔
579
      }
580
      const substedFunctionDeclaration = ast.functionDeclaration(
4,141✔
581
        newID,
582
        substedParams,
583
        dummyBlockStatement()
584
      )
585
      seenBefore.set(target, substedFunctionDeclaration)
4,141✔
586
      let freeReplacement: any[] = []
4,141✔
587
      let boundReplacement: es.Identifier[] = []
4,141✔
588
      if (
4,141✔
589
        replacement.type == 'FunctionExpression' ||
4,737✔
590
        replacement.type == 'ArrowFunctionExpression'
591
      ) {
592
        freeReplacement = findMain(replacement, new Map())
3,545✔
593
        boundReplacement = scanOutBoundNames(replacement.body)
3,545✔
594
      }
595
      const freeTarget = findMain(target, new Map())
4,141✔
596
      const boundTarget = scanOutBoundNames(target.body)
4,141✔
597
      for (let i = 0; i < target.params.length; i++) {
4,141✔
598
        const param = target.params[i]
8,135✔
599
        if (param.type === 'Identifier' && param.name === name.name) {
8,135✔
600
          substedFunctionDeclaration.body = target.body
7✔
601
          return substedFunctionDeclaration
7✔
602
        }
603
        if (param.type == 'Identifier') {
8,128✔
604
          if (freeReplacement.includes(param.name)) {
8,128✔
605
            // change param name
606
            const re = /_\d+$/
14✔
607
            let newNum
608
            if (re.test(param.name)) {
14✔
609
              const num = param.name.split('_')
12✔
610
              newNum = Number(num[1]) + 1
12✔
611
              const changedName: string = getFreshName(
12✔
612
                num[0],
613
                newNum,
614
                freeTarget,
615
                freeReplacement,
616
                boundTarget,
617
                boundUpperScope,
618
                boundReplacement
619
              )
620
              const changed = ast.identifier(changedName, param.loc)
12✔
621
              newBody = substituteMain(param, changed, target.body, [[]])[0] as es.BlockStatement
12✔
622
              ;(substedFunctionDeclaration.params[i] as es.Identifier).name = changedName
12✔
623
            } else {
624
              newNum = 1
2✔
625
              const changedName: string = getFreshName(
2✔
626
                param.name,
627
                newNum,
628
                freeTarget,
629
                freeReplacement,
630
                boundTarget,
631
                boundUpperScope,
632
                boundReplacement
633
              )
634
              const changed = ast.identifier(changedName, param.loc)
2✔
635
              newBody = substituteMain(param, changed, target.body, [[]])[0] as es.BlockStatement
2✔
636
              ;(substedFunctionDeclaration.params[i] as es.Identifier).name = changedName
2✔
637
            }
638
          }
639
        }
640
      }
641

642
      for (const param of substedParams) {
4,134✔
643
        boundUpperScope.push(param.name)
8,128✔
644
      }
645

646
      if (pathNotEnded(index)) {
4,134✔
647
        allPaths[index].push('body')
4,120✔
648
      }
649
      substedFunctionDeclaration.body = substitute(newBody, index) as es.BlockStatement
4,134✔
650
      return substedFunctionDeclaration
4,134✔
651
    },
652

653
    FunctionExpression(target: es.FunctionExpression, index: number): es.FunctionExpression {
654
      const substedParams: es.Identifier[] = []
1,626✔
655
      // creates a copy of the params so that the renaming only happens during substitution.
656
      for (let i = 0; i < target.params.length; i++) {
1,626✔
657
        const param = target.params[i] as es.Identifier
2,899✔
658
        substedParams.push(ast.identifier(param.name, param.loc))
2,899✔
659
      }
660
      const substedFunctionExpression = target.id
1,626✔
661
        ? ast.functionDeclarationExpression(target.id, substedParams, dummyBlockStatement())
1,626!
662
        : ast.functionExpression(substedParams, dummyBlockStatement())
663
      seenBefore.set(target, substedFunctionExpression)
1,626✔
664
      // check for free/bounded variable in replacement
665
      let freeReplacement: any[] = []
1,626✔
666
      let boundReplacement: es.Identifier[] = []
1,626✔
667
      if (
1,626✔
668
        replacement.type == 'FunctionExpression' ||
2,051✔
669
        replacement.type == 'ArrowFunctionExpression'
670
      ) {
671
        freeReplacement = findMain(replacement, new Map())
1,244✔
672
        boundReplacement = scanOutBoundNames(replacement.body)
1,244✔
673
      }
674
      const freeTarget = findMain(target, new Map())
1,626✔
675
      const boundTarget = scanOutBoundNames(target.body)
1,626✔
676
      for (let i = 0; i < target.params.length; i++) {
1,626✔
677
        const param = target.params[i]
2,779✔
678
        if (param.type === 'Identifier' && param.name === name.name) {
2,779✔
679
          substedFunctionExpression.body = target.body
256✔
680
          return substedFunctionExpression
256✔
681
        }
682
        if (param.type == 'Identifier') {
2,523✔
683
          if (freeReplacement.includes(param.name)) {
2,523✔
684
            // change param name
685
            const re = /_\d+$/
6✔
686
            let newNum
687
            if (re.test(param.name)) {
6✔
688
              const num = param.name.split('_')
2✔
689
              newNum = Number(num[1]) + 1
2✔
690
              const changedName: string = getFreshName(
2✔
691
                num[0],
692
                newNum,
693
                freeTarget,
694
                freeReplacement,
695
                boundTarget,
696
                boundUpperScope,
697
                boundReplacement
698
              )
699
              const changed = ast.identifier(changedName, param.loc)
2✔
700
              target.body = substituteMain(param, changed, target.body, [
2✔
701
                []
702
              ])[0] as es.BlockStatement
703
              ;(substedFunctionExpression.params[i] as es.Identifier).name = changedName
2✔
704
            } else {
705
              newNum = 1
4✔
706
              const changedName: string = getFreshName(
4✔
707
                param.name,
708
                newNum,
709
                freeTarget,
710
                freeReplacement,
711
                boundTarget,
712
                boundUpperScope,
713
                boundReplacement
714
              )
715
              const changed = ast.identifier(changedName, param.loc)
4✔
716
              target.body = substituteMain(param, changed, target.body, [
4✔
717
                []
718
              ])[0] as es.BlockStatement
719
              ;(substedFunctionExpression.params[i] as es.Identifier).name = changedName
4✔
720
            }
721
          }
722
        }
723
      }
724

725
      for (const param of substedParams) {
1,370✔
726
        boundUpperScope.push(param.name)
2,416✔
727
      }
728

729
      if (pathNotEnded(index)) {
1,370✔
730
        allPaths[index].push('body')
1,063✔
731
      }
732
      substedFunctionExpression.body = substitute(target.body, index) as es.BlockStatement
1,370✔
733
      return substedFunctionExpression
1,370✔
734
    },
735

736
    Program(target: es.Program, index: number): es.Program {
737
      const substedBody = target.body.map(() => dummyStatement())
5,275✔
738
      const substedProgram = ast.program(substedBody)
826✔
739
      seenBefore.set(target, substedProgram)
826✔
740
      const declaredNames: Set<string> = getDeclaredNames(target)
826✔
741
      const re = / same/
826✔
742
      // checks if the replacement is a functionExpression or arrowFunctionExpression and not from within the same block
743
      if (
826!
744
        (replacement.type == 'FunctionExpression' ||
1,673✔
745
          replacement.type == 'ArrowFunctionExpression') &&
746
        !re.test(name.name)
747
      ) {
748
        const freeTarget: string[] = findMain(target, new Map())
×
749
        const declaredIds: es.Identifier[] = scanOutDeclarations(target)
×
750
        const freeReplacement: string[] = findMain(replacement, new Map())
×
751
        const boundReplacement: es.Identifier[] = scanOutDeclarations(replacement.body)
×
752
        for (const declaredId of declaredIds) {
×
753
          if (freeReplacement.includes(declaredId.name)) {
×
754
            const re = /_\d+$/
×
755
            let newNum
756
            if (re.test(declaredId.name)) {
×
757
              const num = declaredId.name.split('_')
×
758
              newNum = Number(num[1]) + 1
×
759
              const changedName: string = getFreshName(
×
760
                num[0],
761
                newNum,
762
                freeTarget,
763
                freeReplacement,
764
                declaredIds,
765
                boundUpperScope,
766
                boundReplacement
767
              )
768
              const changed = ast.identifier(changedName + ' rename', declaredId.loc)
×
769
              const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
×
770
              target = substituteMain(newName, changed, target, [[]])[0] as es.Program
×
771
            } else {
772
              newNum = 1
×
773
              const changedName: string = getFreshName(
×
774
                declaredId.name,
775
                newNum,
776
                freeTarget,
777
                freeReplacement,
778
                declaredIds,
779
                boundUpperScope,
780
                boundReplacement
781
              )
782
              const changed = ast.identifier(changedName + ' rename', declaredId.loc)
×
783
              const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
×
784
              target = substituteMain(newName, changed, target, [[]])[0] as es.Program
×
785
            }
786
          }
787
        }
788
      }
789

790
      const re2 = / rename/
826✔
791
      if (declaredNames.has(name.name) && !re2.test(name.name)) {
826!
792
        substedProgram.body = target.body
×
793
        return substedProgram
×
794
      }
795

796
      // if it is from the same block then the name would be name + " same", hence need to remove " same"
797
      // if not this statement does nothing as variable names should not have spaces
798
      name.name = name.name.split(' ')[0]
826✔
799

800
      const arr: number[] = []
826✔
801
      let nextIndex = index
826✔
802
      for (let i = 1; i < target.body.length; i++) {
826✔
803
        if (pathNotEnded(index)) {
4,449✔
804
          nextIndex = branch(index)
4,449✔
805
          allPaths[nextIndex].push('body[' + i + ']')
4,449✔
806
        }
807
        arr[i] = nextIndex
4,449✔
808
      }
809
      if (pathNotEnded(index)) {
826✔
810
        allPaths[index].push('body[0]')
826✔
811
      }
812
      arr[0] = index
826✔
813
      let arrIndex = -1
826✔
814
      substedProgram.body = target.body.map(stmt => {
826✔
815
        arrIndex++
5,275✔
816
        return substitute(stmt, arr[arrIndex]) as es.Statement
5,275✔
817
      })
818
      return substedProgram
826✔
819
    },
820

821
    BlockStatement(target: es.BlockStatement, index: number): es.BlockStatement {
822
      const substedBody = target.body.map(() => dummyStatement())
9,900✔
823
      const substedBlockStatement = ast.blockStatement(substedBody)
7,717✔
824
      seenBefore.set(target, substedBlockStatement)
7,717✔
825
      const declaredNames: Set<string> = getDeclaredNames(target)
7,717✔
826
      const re = / same/
7,717✔
827
      // checks if the replacement is a functionExpression or arrowFunctionExpression and not from within the same block
828
      if (
7,717✔
829
        (replacement.type == 'FunctionExpression' ||
15,490✔
830
          replacement.type == 'ArrowFunctionExpression') &&
831
        !re.test(name.name)
832
      ) {
833
        const freeTarget: string[] = findMain(target, new Map())
6,501✔
834
        const declaredIds: es.Identifier[] = scanOutDeclarations(target)
6,501✔
835
        const freeReplacement: string[] = findMain(replacement, new Map())
6,501✔
836
        const boundReplacement: es.Identifier[] = scanOutDeclarations(replacement.body)
6,501✔
837
        for (const declaredId of declaredIds) {
6,501✔
838
          if (freeReplacement.includes(declaredId.name)) {
1,665✔
839
            const re = /_\d+$/
2✔
840
            let newNum
841
            if (re.test(declaredId.name)) {
2!
842
              const num = declaredId.name.split('_')
×
843
              newNum = Number(num[1]) + 1
×
844
              const changedName: string = getFreshName(
×
845
                num[0],
846
                newNum,
847
                freeTarget,
848
                freeReplacement,
849
                declaredIds,
850
                boundUpperScope,
851
                boundReplacement
852
              )
853
              const changed = ast.identifier(changedName + ' rename', declaredId.loc)
×
854
              const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
×
855
              target = substituteMain(newName, changed, target, [[]])[0] as es.BlockStatement
×
856
            } else {
857
              newNum = 1
2✔
858
              const changedName: string = getFreshName(
2✔
859
                declaredId.name,
860
                newNum,
861
                freeTarget,
862
                freeReplacement,
863
                declaredIds,
864
                boundUpperScope,
865
                boundReplacement
866
              )
867
              const changed = ast.identifier(changedName + ' rename', declaredId.loc)
2✔
868
              const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
2✔
869
              target = substituteMain(newName, changed, target, [[]])[0] as es.BlockStatement
2✔
870
            }
871
          }
872
        }
873
      }
874

875
      const re2 = / rename/
7,717✔
876
      if (declaredNames.has(name.name) && !re2.test(name.name)) {
7,717✔
877
        substedBlockStatement.body = target.body
8✔
878
        return substedBlockStatement
8✔
879
      }
880

881
      // if it is from the same block then the name would be name + " same", hence need to remove " same"
882
      // if not this statement does nothing as variable names should not have spaces
883
      name.name = name.name.split(' ')[0]
7,709✔
884

885
      const arr: number[] = []
7,709✔
886
      let nextIndex = index
7,709✔
887
      for (let i = 1; i < target.body.length; i++) {
7,709✔
888
        if (pathNotEnded(index)) {
2,252✔
889
          nextIndex = branch(index)
2,176✔
890
          allPaths[nextIndex].push('body[' + i + ']')
2,176✔
891
        }
892
        arr[i] = nextIndex
2,252✔
893
      }
894
      if (pathNotEnded(index)) {
7,709✔
895
        allPaths[index].push('body[0]')
7,348✔
896
      }
897
      arr[0] = index
7,709✔
898
      let arrIndex = -1
7,709✔
899
      substedBlockStatement.body = target.body.map(stmt => {
7,709✔
900
        arrIndex++
9,884✔
901
        return substitute(stmt, arr[arrIndex]) as es.Statement
9,884✔
902
      })
903
      return substedBlockStatement
7,709✔
904
    },
905

906
    BlockExpression(target: BlockExpression, index: number): BlockExpression {
907
      const substedBody = target.body.map(() => dummyStatement())
32✔
908
      const substedBlockExpression = ast.blockExpression(substedBody)
30✔
909
      seenBefore.set(target, substedBlockExpression)
30✔
910
      const declaredNames: Set<string> = getDeclaredNames(target)
30✔
911
      const re = / same/
30✔
912
      // checks if the replacement is a functionExpression or arrowFunctionExpression and not from within the same block
913
      if (
30!
914
        (replacement.type == 'FunctionExpression' ||
61✔
915
          replacement.type == 'ArrowFunctionExpression') &&
916
        !re.test(name.name)
917
      ) {
918
        const freeTarget: string[] = findMain(target, new Map())
×
919
        const declaredIds: es.Identifier[] = scanOutDeclarations(target)
×
920
        const freeReplacement: string[] = findMain(replacement, new Map())
×
921
        const boundReplacement: es.Identifier[] = scanOutDeclarations(replacement.body)
×
922
        for (const declaredId of declaredIds) {
×
923
          if (freeReplacement.includes(declaredId.name)) {
×
924
            const re = /_\d+$/
×
925
            let newNum
926
            if (re.test(declaredId.name)) {
×
927
              const num = declaredId.name.split('_')
×
928
              newNum = Number(num[1]) + 1
×
929
              const changedName: string = getFreshName(
×
930
                num[0],
931
                newNum,
932
                freeTarget,
933
                freeReplacement,
934
                declaredIds,
935
                boundUpperScope,
936
                boundReplacement
937
              )
938
              const changed = ast.identifier(changedName + ' rename', declaredId.loc)
×
939
              const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
×
940
              target = substituteMain(newName, changed, target, [[]])[0] as BlockExpression
×
941
            } else {
942
              newNum = 1
×
943
              const changedName: string = getFreshName(
×
944
                declaredId.name,
945
                newNum,
946
                freeTarget,
947
                freeReplacement,
948
                declaredIds,
949
                boundUpperScope,
950
                boundReplacement
951
              )
952
              const changed = ast.identifier(changedName + ' rename', declaredId.loc)
×
953
              const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
×
954
              target = substituteMain(newName, changed, target, [[]])[0] as BlockExpression
×
955
            }
956
          }
957
        }
958
      }
959

960
      const re2 = / rename/
30✔
961
      if (declaredNames.has(name.name) && !re2.test(name.name)) {
30!
962
        substedBlockExpression.body = target.body
×
963
        return substedBlockExpression
×
964
      }
965

966
      // if it is from the same block then the name would be name + " same", hence need to remove " same"
967
      // if not this statement does nothing as variable names should not have spaces
968
      name.name = name.name.split(' ')[0]
30✔
969

970
      const arr: number[] = []
30✔
971
      let nextIndex = index
30✔
972
      for (let i = 1; i < target.body.length; i++) {
30✔
973
        if (pathNotEnded(index)) {
2✔
974
          nextIndex = branch(index)
2✔
975
          allPaths[nextIndex].push('body[' + i + ']')
2✔
976
        }
977
        arr[i] = nextIndex
2✔
978
      }
979
      if (pathNotEnded(index)) {
30✔
980
        allPaths[index].push('body[0]')
30✔
981
      }
982
      arr[0] = index
30✔
983
      let arrIndex = -1
30✔
984
      substedBlockExpression.body = target.body.map(stmt => {
30✔
985
        arrIndex++
32✔
986
        return substitute(stmt, arr[arrIndex]) as es.Statement
32✔
987
      })
988
      return substedBlockExpression
30✔
989
    },
990

991
    ReturnStatement(target: es.ReturnStatement, index: number): es.ReturnStatement {
992
      const substedReturnStatement = ast.returnStatement(dummyExpression(), target.loc)
6,615✔
993
      seenBefore.set(target, substedReturnStatement)
6,615✔
994
      if (pathNotEnded(index)) {
6,615✔
995
        allPaths[index].push('argument')
6,276✔
996
      }
997
      substedReturnStatement.argument = substitute(target.argument!, index) as es.Expression
6,615✔
998
      return substedReturnStatement
6,615✔
999
    },
1000

1001
    // source 1
1002
    ArrowFunctionExpression(
1003
      target: es.ArrowFunctionExpression,
1004
      index: number
1005
    ): es.ArrowFunctionExpression {
1006
      // creates a copy of the parameters so that renaming only happens during substitution
1007
      const substedParams: es.Identifier[] = []
1,976✔
1008
      for (let i = 0; i < target.params.length; i++) {
1,976✔
1009
        const param = target.params[i] as es.Identifier
1,419✔
1010
        substedParams.push(ast.identifier(param.name, param.loc))
1,419✔
1011
      }
1012
      let newBody = target.body
1,976✔
1013
      const substedArrow = ast.arrowFunctionExpression(substedParams, dummyBlockStatement())
1,976✔
1014
      seenBefore.set(target, substedArrow)
1,976✔
1015
      // check for free/bounded variable
1016
      let freeReplacement: string[] = []
1,976✔
1017
      let boundReplacement: es.Identifier[] = []
1,976✔
1018
      if (
1,976✔
1019
        replacement.type == 'FunctionExpression' ||
2,551✔
1020
        replacement.type == 'ArrowFunctionExpression'
1021
      ) {
1022
        freeReplacement = findMain(replacement, new Map())
1,511✔
1023
        boundReplacement = scanOutBoundNames(replacement.body)
1,511✔
1024
      }
1025
      for (let i = 0; i < target.params.length; i++) {
1,976✔
1026
        const param = target.params[i]
1,419✔
1027
        if (param.type === 'Identifier' && param.name === name.name) {
1,419✔
1028
          substedArrow.body = target.body
9✔
1029
          substedArrow.expression = target.body.type !== 'BlockStatement'
9✔
1030
          return substedArrow
9✔
1031
        }
1032
        const freeTarget = findMain(target, new Map())
1,410✔
1033
        const boundTarget = scanOutBoundNames(target.body)
1,410✔
1034
        if (param.type == 'Identifier') {
1,410✔
1035
          if (freeReplacement.includes(param.name)) {
1,410✔
1036
            // change param name
1037
            const re = /_\d+$/
14✔
1038
            let newNum
1039
            if (re.test(param.name)) {
14✔
1040
              const num = param.name.split('_')
9✔
1041
              newNum = Number(num[1]) + 1
9✔
1042
              const changedName: string = getFreshName(
9✔
1043
                num[0],
1044
                newNum,
1045
                freeTarget,
1046
                freeReplacement,
1047
                boundTarget,
1048
                boundUpperScope,
1049
                boundReplacement
1050
              )
1051
              const changed = ast.identifier(changedName, param.loc)
9✔
1052
              newBody = substituteMain(param, changed, target.body, [[]])[0] as es.BlockStatement
9✔
1053
              ;(substedArrow.params[i] as es.Identifier).name = changedName // num[0] + '_' + newNum
9✔
1054
            } else {
1055
              newNum = 1
5✔
1056
              const changedName: string = getFreshName(
5✔
1057
                param.name,
1058
                newNum,
1059
                freeTarget,
1060
                freeReplacement,
1061
                boundTarget,
1062
                boundUpperScope,
1063
                boundReplacement
1064
              )
1065
              const changed = ast.identifier(changedName, param.loc)
5✔
1066
              newBody = substituteMain(param, changed, target.body, [[]])[0] as es.BlockStatement
5✔
1067
              ;(substedArrow.params[i] as es.Identifier).name = changedName
5✔
1068
            }
1069
          }
1070
        }
1071
      }
1072

1073
      for (const param of substedParams) {
1,967✔
1074
        boundUpperScope.push(param.name)
1,410✔
1075
      }
1076

1077
      for (const param of target.params) {
1,967✔
1078
        if (param.type === 'Identifier' && param.name === name.name) {
1,410!
1079
          substedArrow.body = target.body
×
1080
          substedArrow.expression = target.body.type !== 'BlockStatement'
×
1081
          return substedArrow
×
1082
        }
1083
      }
1084
      if (pathNotEnded(index)) {
1,967✔
1085
        allPaths[index].push('body')
1,818✔
1086
      }
1087
      substedArrow.body = substitute(newBody, index) as es.BlockStatement | es.Expression
1,967✔
1088
      substedArrow.expression = target.body.type !== 'BlockStatement'
1,967✔
1089
      return substedArrow
1,967✔
1090
    },
1091

1092
    VariableDeclaration(target: es.VariableDeclaration, index: number): es.VariableDeclaration {
1093
      const substedVariableDeclaration = ast.variableDeclaration([dummyVariableDeclarator()])
2,225✔
1094
      seenBefore.set(target, substedVariableDeclaration)
2,225✔
1095
      const arr: number[] = []
2,225✔
1096
      let nextIndex = index
2,225✔
1097
      for (let i = 1; i < target.declarations.length; i++) {
2,225✔
1098
        if (pathNotEnded(index)) {
×
1099
          nextIndex = branch(index)
×
1100
          allPaths[nextIndex].push('declarations[' + i + ']')
×
1101
        }
1102
        arr[i] = nextIndex
×
1103
      }
1104
      if (pathNotEnded(index)) {
2,225✔
1105
        allPaths[index].push('declarations[0]')
2,163✔
1106
      }
1107
      arr[0] = index
2,225✔
1108
      let arrIndex = -1
2,225✔
1109
      substedVariableDeclaration.declarations = target.declarations.map(dec => {
2,225✔
1110
        arrIndex++
2,225✔
1111
        return substitute(dec, arr[arrIndex]) as es.VariableDeclarator
2,225✔
1112
      })
1113
      return substedVariableDeclaration
2,225✔
1114
    },
1115

1116
    VariableDeclarator(target: es.VariableDeclarator, index: number): es.VariableDeclarator {
1117
      const subbed = ast.identifier((target.id as es.Identifier).name)
2,225✔
1118
      let substedVariableDeclarator = ast.variableDeclarator(subbed, dummyExpression())
2,225✔
1119
      seenBefore.set(target, substedVariableDeclarator)
2,225✔
1120
      const re = / rename$/
2,225✔
1121
      if (target.id.type === 'Identifier' && name.name === target.id.name) {
2,225✔
1122
        if (replacement.type == 'Identifier' && re.test(replacement.name)) {
1✔
1123
          const newName = ast.identifier(replacement.name.split(' ')[0], replacement.loc)
1✔
1124
          substedVariableDeclarator = ast.variableDeclarator(newName, dummyExpression())
1✔
1125
        }
1126
        substedVariableDeclarator.init = target.init
1✔
1127
      } else {
1128
        if (pathNotEnded(index)) {
2,224✔
1129
          allPaths[index].push('init')
2,162✔
1130
        }
1131
        substedVariableDeclarator.init = substitute(target.init!, index) as es.Expression
2,224✔
1132
      }
1133
      return substedVariableDeclarator
2,225✔
1134
    },
1135

1136
    IfStatement(target: es.IfStatement, index: number): es.IfStatement {
1137
      const substedIfStatement = ast.ifStatement(
784✔
1138
        dummyExpression(),
1139
        dummyBlockStatement(),
1140
        dummyBlockStatement(),
1141
        target.loc
1142
      )
1143
      seenBefore.set(target, substedIfStatement)
784✔
1144
      let nextIndex = index
784✔
1145
      let thirdIndex = index
784✔
1146
      if (pathNotEnded(index)) {
784✔
1147
        nextIndex = branch(index)
767✔
1148
        thirdIndex = branch(index)
767✔
1149
        allPaths[index].push('test')
767✔
1150
        allPaths[nextIndex].push('consequent')
767✔
1151
        allPaths[thirdIndex].push('alternate')
767✔
1152
      }
1153
      substedIfStatement.test = substitute(target.test, index) as es.Expression
784✔
1154
      substedIfStatement.consequent = substitute(target.consequent, nextIndex) as es.BlockStatement
784✔
1155
      substedIfStatement.alternate = target.alternate
784✔
1156
        ? (substitute(target.alternate, thirdIndex) as es.BlockStatement)
784!
1157
        : null
1158
      return substedIfStatement
784✔
1159
    },
1160

1161
    ArrayExpression(target: es.ArrayExpression, index: number): es.ArrayExpression {
1162
      const substedArray = ast.arrayExpression([dummyExpression()])
882✔
1163
      seenBefore.set(target, substedArray)
882✔
1164
      const arr: number[] = []
882✔
1165
      let nextIndex = index
882✔
1166
      for (let i = 1; i < target.elements.length; i++) {
882✔
1167
        if (pathNotEnded(index)) {
882✔
1168
          nextIndex = branch(index)
324✔
1169
          allPaths[nextIndex].push('elements[' + i + ']')
324✔
1170
        }
1171
        arr[i] = nextIndex
882✔
1172
      }
1173
      if (pathNotEnded(index)) {
882✔
1174
        allPaths[index].push('elements[0]')
324✔
1175
      }
1176
      arr[0] = index
882✔
1177
      let arrIndex = -1
882✔
1178
      substedArray.elements = target.elements.map(ele => {
882✔
1179
        arrIndex++
1,764✔
1180
        return substitute(ele as ContiguousArrayElementExpression, arr[arrIndex]) as es.Expression
1,764✔
1181
      })
1182
      return substedArray
882✔
1183
    }
1184
  }
1185

1186
  /**
1187
   * For mapper use, maps a [symbol, value] pair to the node supplied.
1188
   * @param name the name to be replaced
1189
   * @param replacement the expression to replace the name with
1190
   * @param node a node holding the target symbols
1191
   * @param seenBefore a list of nodes that are seen before in substitution
1192
   */
1193
  function substitute(target: substituterNodes, index: number): substituterNodes {
1194
    const result = seenBefore.get(target)
114,908✔
1195
    if (result) {
114,908✔
1196
      return result as substituterNodes
1,631✔
1197
    }
1198
    const substituter = substituters[target.type]
113,277✔
1199
    if (substituter === undefined) {
113,277✔
1200
      seenBefore.set(target, target)
7,665✔
1201
      return target // no need to subst, such as literals
7,665✔
1202
    } else {
1203
      // substituters are responsible of registering seenBefore
1204
      return substituter(target, index)
105,612✔
1205
    }
1206
  }
1207

1208
  // after running substitute,
1209
  // find paths that contain endMarker
1210
  // and return only those paths
1211
  const substituted = substitute(target, 0)
1,503✔
1212
  const validPaths: string[][] = []
1,503✔
1213
  for (const path of allPaths) {
1,503✔
1214
    if (path[path.length - 1] === endMarker) {
51,479✔
1215
      validPaths.push(path.slice(0, path.length - 1))
1,170✔
1216
    }
1217
  }
1218
  return [substituted, validPaths]
1,503✔
1219
}
1220

1221
/**
1222
 * Substitutes a call expression with the body of the callee (funExp)
1223
 * and the body will have all ocurrences of parameters substituted
1224
 * with the arguments.
1225
 * @param callee call expression with callee as functionExpression
1226
 * @param args arguments supplied to the call expression
1227
 */
1228
function apply(
1229
  callee: es.FunctionExpression | es.ArrowFunctionExpression,
1230
  args: irreducibleNodes[]
1231
): BlockExpression | es.Expression {
1232
  let substedBody = callee.body
774✔
1233
  let substedParams = callee.params
774✔
1234
  for (let i = 0; i < args.length; i++) {
774✔
1235
    // source discipline requires parameters to be identifiers.
1236
    const arg = args[i]
330✔
1237

1238
    if (arg.type === 'ArrowFunctionExpression' || arg.type === 'FunctionExpression') {
330✔
1239
      const freeTarget: string[] = findMain(
31✔
1240
        ast.arrowFunctionExpression(substedParams, substedBody),
1241
        new Map()
1242
      )
1243
      const declaredIds: es.Identifier[] = substedParams as es.Identifier[]
31✔
1244
      const freeReplacement: string[] = findMain(arg, new Map())
31✔
1245
      const boundReplacement: es.Identifier[] = scanOutDeclarations(arg.body)
31✔
1246
      for (const declaredId of declaredIds) {
31✔
1247
        if (freeReplacement.includes(declaredId.name)) {
85✔
1248
          const re = /_\d+$/
1✔
1249
          let newNum
1250
          if (re.test(declaredId.name)) {
1!
1251
            const num = declaredId.name.split('_')
×
1252
            newNum = Number(num[1]) + 1
×
1253
            const changedName: string = getFreshName(
×
1254
              num[0],
1255
              newNum,
1256
              freeTarget,
1257
              freeReplacement,
1258
              declaredIds,
1259
              [],
1260
              boundReplacement
1261
            )
1262
            const changed = ast.identifier(changedName + ' rename', declaredId.loc)
×
1263
            const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
×
1264
            substedBody = substituteMain(newName, changed, substedBody, [
×
1265
              []
1266
            ])[0] as typeof substedBody
1267
            substedParams = substedParams.map(param =>
×
1268
              (param as es.Identifier).name === declaredId.name ? changed : param
×
1269
            )
1270
          } else {
1271
            newNum = 1
1✔
1272
            const changedName: string = getFreshName(
1✔
1273
              declaredId.name,
1274
              newNum,
1275
              freeTarget,
1276
              freeReplacement,
1277
              declaredIds,
1278
              [],
1279
              boundReplacement
1280
            )
1281
            const changed = ast.identifier(changedName + ' rename', declaredId.loc)
1✔
1282
            const newName = ast.identifier(declaredId.name + ' rename', declaredId.loc)
1✔
1283
            substedBody = substituteMain(newName, changed, substedBody, [
1✔
1284
              []
1285
            ])[0] as typeof substedBody
1286
            substedParams = substedParams.map(param =>
1✔
1287
              (param as es.Identifier).name === declaredId.name ? changed : param
2✔
1288
            )
1289
          }
1290
        }
1291
      }
1292
    }
1293

1294
    // source discipline requires parameters to be identifiers.
1295
    const param = substedParams[i] as es.Identifier
330✔
1296
    substedBody = substituteMain(param, arg, substedBody, [[]])[0] as typeof substedBody
330✔
1297
  }
1298
  if (callee.type === 'ArrowFunctionExpression' && callee.expression) {
774✔
1299
    return substedBody as es.Expression
45✔
1300
  }
1301

1302
  const firstStatement: es.Statement = (substedBody as es.BlockStatement).body[0]
729✔
1303
  return firstStatement && firstStatement.type === 'ReturnStatement'
729✔
1304
    ? (firstStatement.argument as es.Expression)
729✔
1305
    : ast.blockExpression((substedBody as es.BlockStatement).body)
1306
}
1307

1308
// Wrapper function to house reduce, explain and bodify
1309
function reduceMain(
1310
  node: substituterNodes,
1311
  context: Context
1312
): [substituterNodes, Context, string[][], string] {
1313
  // variable to control verbosity of bodify
1314
  let verbose = true
1,956✔
1315

1316
  // converts body of code to string
1317
  function bodify(target: substituterNodes): string {
1318
    const bodifiers = {
5,994✔
1319
      Literal: (target: es.Literal): string =>
1320
        target.raw !== undefined ? target.raw : String(target.value),
2,128✔
1321

1322
      Identifier: (target: es.Identifier): string =>
1323
        target.name.startsWith('anonymous_') ? 'anonymous function' : target.name,
1,287!
1324

1325
      ExpressionStatement: (target: es.ExpressionStatement): string =>
1326
        bodify(target.expression) + ' finished evaluating',
58✔
1327

1328
      BinaryExpression: (target: es.BinaryExpression): string =>
1329
        bodify(target.left) + ' ' + target.operator + ' ' + bodify(target.right),
484✔
1330

1331
      UnaryExpression: (target: es.UnaryExpression): string =>
1332
        target.operator + bodify(target.argument),
8✔
1333

1334
      ConditionalExpression: (target: es.ConditionalExpression): string =>
1335
        bodify(target.test) + ' ? ' + bodify(target.consequent) + ' : ' + bodify(target.alternate),
11✔
1336

1337
      LogicalExpression: (target: es.LogicalExpression): string =>
1338
        bodify(target.left) + ' ' + target.operator + ' ' + bodify(target.right),
1✔
1339

1340
      CallExpression: (target: es.CallExpression): string => {
1341
        if (target.callee.type === 'ArrowFunctionExpression') {
129✔
1342
          return '(' + bodify(target.callee) + ')(' + target.arguments.map(bodify) + ')'
28✔
1343
        } else {
1344
          return bodify(target.callee) + '(' + target.arguments.map(bodify) + ')'
101✔
1345
        }
1346
      },
1347

1348
      FunctionDeclaration: (target: es.FunctionDeclaration): string => {
1349
        const funcName = target.id !== null ? target.id.name : 'error'
277!
1350
        return (
277✔
1351
          'Function ' +
1352
          funcName +
1353
          ' declared' +
1354
          (target.params.length > 0
1355
            ? ', parameter(s) ' + target.params.map(bodify) + ' required'
277✔
1356
            : '')
1357
        )
1358
      },
1359

1360
      FunctionExpression: (target: es.FunctionExpression): string => {
1361
        const id = target.id
782✔
1362
        return id === null || id === undefined ? '...' : id.name
782!
1363
      },
1364

1365
      ReturnStatement: (target: es.ReturnStatement): string =>
1366
        bodify(target.argument as es.Expression) + ' returned',
34✔
1367

1368
      // guards against infinite text generation
1369
      ArrowFunctionExpression: (target: es.ArrowFunctionExpression): string => {
1370
        if (verbose) {
113✔
1371
          verbose = false
81✔
1372
          const redacted =
1373
            (target.params.length > 0 ? target.params.map(bodify) : '()') +
81✔
1374
            ' => ' +
1375
            bodify(target.body)
1376
          verbose = true
81✔
1377
          return redacted
81✔
1378
        } else {
1379
          return (target.params.length > 0 ? target.params.map(bodify) : '()') + ' => ...'
32✔
1380
        }
1381
      },
1382

1383
      VariableDeclaration: (target: es.VariableDeclaration): string =>
1384
        'Constant ' +
78✔
1385
        bodify(target.declarations[0].id) +
1386
        ' declared and substituted into rest of block',
1387

1388
      ArrayExpression: (target: es.ArrayExpression): string =>
1389
        '[' +
599✔
1390
        bodify(target.elements[0] as ContiguousArrayElementExpression) +
1391
        ', ' +
1392
        bodify(target.elements[1] as ContiguousArrayElementExpression) +
1393
        ']'
1394
    }
1395

1396
    const bodifier = bodifiers[target.type]
5,994✔
1397
    return bodifier === undefined ? '...' : bodifier(target)
5,994✔
1398
  }
1399

1400
  // generates string to explain current step
1401
  function explain(target: substituterNodes): string {
1402
    const explainers = {
1,949✔
1403
      BinaryExpression: (target: es.BinaryExpression): string =>
1404
        'Binary expression ' + bodify(target) + ' evaluated',
424✔
1405

1406
      UnaryExpression: (target: es.UnaryExpression): string => {
1407
        return (
3✔
1408
          'Unary expression evaluated, ' +
1409
          (target.operator === '!' ? 'boolean ' : 'value ') +
3!
1410
          bodify(target.argument) +
1411
          ' negated'
1412
        )
1413
      },
1414

1415
      ConditionalExpression: (target: es.ConditionalExpression): string => {
1416
        return (
156✔
1417
          'Conditional expression evaluated, condition is ' +
1418
          (bodify(target.test) === 'true'
1419
            ? 'true, consequent evaluated'
156✔
1420
            : 'false, alternate evaluated')
1421
        )
1422
      },
1423

1424
      LogicalExpression: (target: es.LogicalExpression): string => {
1425
        return target.operator === '&&'
2✔
1426
          ? 'AND operation evaluated, left of operator is ' +
2!
1427
              (bodify(target.left) === 'true'
1428
                ? 'true, continue evaluating right of operator'
×
1429
                : 'false, stop evaluation')
1430
          : 'OR operation evaluated, left of operator is ' +
1431
              (bodify(target.left) === 'true'
1432
                ? 'true, stop evaluation'
2!
1433
                : 'false, continue evaluating right of operator')
1434
      },
1435

1436
      CallExpression: (target: es.CallExpression): string => {
1437
        if (target.callee.type === 'ArrowFunctionExpression') {
904✔
1438
          if (target.callee.params.length === 0) {
50✔
1439
            return bodify(target.callee) + ' runs'
11✔
1440
          } else {
1441
            return (
39✔
1442
              target.arguments.map(bodify) +
1443
              ' substituted into ' +
1444
              target.callee.params.map(bodify) +
1445
              ' of ' +
1446
              bodify(target.callee)
1447
            )
1448
          }
1449
        } else if (target.callee.type === 'FunctionExpression') {
854✔
1450
          if (target.callee.params.length === 0) {
724✔
1451
            return 'Function ' + bodify(target.callee) + ' runs'
513✔
1452
          } else {
1453
            return (
211✔
1454
              'Function ' +
1455
              bodify(target.callee) +
1456
              ' takes in ' +
1457
              target.arguments.map(bodify) +
1458
              ' as input ' +
1459
              target.callee.params.map(bodify)
1460
            )
1461
          }
1462
        } else {
1463
          return bodify(target.callee) + ' runs'
130✔
1464
        }
1465
      },
1466

1467
      Program: (target: es.Program): string => bodify(target.body[0]),
377✔
1468

1469
      BlockExpression: (target: BlockExpression): string =>
1470
        target.body.length === 0 ? 'Empty block statement evaluated' : bodify(target.body[0]),
67✔
1471

1472
      BlockStatement: (target: es.BlockStatement): string =>
1473
        target.body.length === 0 ? 'Empty block statement evaluated' : bodify(target.body[0]),
5!
1474

1475
      IfStatement: (target: es.IfStatement): string => {
1476
        return (
11✔
1477
          'If statement evaluated, ' +
1478
          (bodify(target.test) === 'true'
1479
            ? 'condition true, proceed to if block'
11✔
1480
            : 'condition false, proceed to else block')
1481
        )
1482
      }
1483
    }
1484

1485
    const explainer = explainers[target.type]
1,949✔
1486
    return explainer === undefined ? '...' : explainer(target)
1,949!
1487
  }
1488

1489
  const reducers = {
1,956✔
1490
    // source 0
1491
    Identifier(
1492
      node: es.Identifier,
1493
      context: Context,
1494
      paths: string[][]
1495
    ): [substituterNodes, Context, string[][], string] {
1496
      // can only be built ins. the rest should have been declared
1497
      if (
2!
1498
        !(isAllowedLiterals(node) || isBuiltinFunction(node) || isImportedFunction(node, context))
6✔
1499
      ) {
1500
        throw new errors.UndefinedVariable(node.name, node)
2✔
1501
      } else {
1502
        return [node, context, paths, 'identifier']
×
1503
      }
1504
    },
1505

1506
    ExpressionStatement(
1507
      node: es.ExpressionStatement,
1508
      context: Context,
1509
      paths: string[][]
1510
    ): [substituterNodes, Context, string[][], string] {
1511
      paths[0].push('expression')
1,577✔
1512
      const [reduced, cont, path, str] = reduce(node.expression, context, paths)
1,577✔
1513
      return [ast.expressionStatement(reduced as es.Expression), cont, path, str]
1,570✔
1514
    },
1515

1516
    BinaryExpression(
1517
      node: es.BinaryExpression,
1518
      context: Context,
1519
      paths: string[][]
1520
    ): [substituterNodes, Context, string[][], string] {
1521
      const { operator, left, right } = node
25,293✔
1522
      if (isIrreducible(left, context)) {
25,293✔
1523
        if (isIrreducible(right, context)) {
25,235✔
1524
          // if the ast are the same, then the values are the same
1525
          if (
427✔
1526
            builtin.is_function(left).value &&
433✔
1527
            builtin.is_function(right).value &&
1528
            operator === '==='
1529
          ) {
1530
            return [valueToExpression(left === right), context, paths, explain(node)]
3✔
1531
          }
1532
          const [leftValue, rightValue] = [left, right].map(nodeToValue)
424✔
1533
          const error = rttc.checkBinaryExpression(
424✔
1534
            node,
1535
            operator,
1536
            context.chapter,
1537
            leftValue,
1538
            rightValue
1539
          )
1540
          if (error === undefined) {
424✔
1541
            const lit = evaluateBinaryExpression(operator, leftValue, rightValue)
421✔
1542
            return [valueToExpression(lit, context), context, paths, explain(node)]
421✔
1543
          } else {
1544
            throw error
3✔
1545
          }
1546
        } else {
1547
          paths[0].push('right')
24,808✔
1548
          const [reducedRight, cont, path, str] = reduce(right, context, paths)
24,808✔
1549
          const reducedExpression = ast.binaryExpression(
24,808✔
1550
            operator,
1551
            left,
1552
            reducedRight as es.Expression,
1553
            node.loc
1554
          )
1555
          return [reducedExpression, cont, path, str]
24,808✔
1556
        }
1557
      } else {
1558
        paths[0].push('left')
58✔
1559
        const [reducedLeft, cont, path, str] = reduce(left, context, paths)
58✔
1560
        const reducedExpression = ast.binaryExpression(
57✔
1561
          operator,
1562
          reducedLeft as es.Expression,
1563
          right,
1564
          node.loc
1565
        )
1566
        return [reducedExpression, cont, path, str]
57✔
1567
      }
1568
    },
1569

1570
    UnaryExpression(
1571
      node: es.UnaryExpression,
1572
      context: Context,
1573
      paths: string[][]
1574
    ): [substituterNodes, Context, string[][], string] {
1575
      const { operator, argument } = node
6✔
1576
      if (isIrreducible(argument, context)) {
6✔
1577
        // tslint:disable-next-line
1578
        const argumentValue = nodeToValue(argument)
3✔
1579
        const error = rttc.checkUnaryExpression(node, operator, argumentValue, context.chapter)
3✔
1580
        if (error === undefined) {
3!
1581
          const result = evaluateUnaryExpression(operator, argumentValue)
3✔
1582
          return [valueToExpression(result, context), context, paths, explain(node)]
3✔
1583
        } else {
1584
          throw error
×
1585
        }
1586
      } else {
1587
        paths[0].push('argument')
3✔
1588
        const [reducedArgument, cont, path, str] = reduce(argument, context, paths)
3✔
1589
        const reducedExpression = ast.unaryExpression(
3✔
1590
          operator,
1591
          reducedArgument as es.Expression,
1592
          node.loc
1593
        )
1594
        return [reducedExpression, cont, path, str]
3✔
1595
      }
1596
    },
1597

1598
    ConditionalExpression(
1599
      node: es.ConditionalExpression,
1600
      context: Context,
1601
      paths: string[][]
1602
    ): [substituterNodes, Context, string[][], string] {
1603
      const { test, consequent, alternate } = node
316✔
1604
      if (test.type === 'Literal') {
316✔
1605
        const error = rttc.checkIfStatement(node, test.value, context.chapter)
156✔
1606
        if (error === undefined) {
156!
1607
          return [
156✔
1608
            (test.value ? consequent : alternate) as es.Expression,
156✔
1609
            context,
1610
            paths,
1611
            explain(node)
1612
          ]
1613
        } else {
1614
          throw error
×
1615
        }
1616
      } else {
1617
        paths[0].push('test')
160✔
1618
        const [reducedTest, cont, path, str] = reduce(test, context, paths)
160✔
1619
        const reducedExpression = ast.conditionalExpression(
160✔
1620
          reducedTest as es.Expression,
1621
          consequent,
1622
          alternate,
1623
          node.loc
1624
        )
1625
        return [reducedExpression, cont, path, str]
160✔
1626
      }
1627
    },
1628

1629
    LogicalExpression(
1630
      node: es.LogicalExpression,
1631
      context: Context,
1632
      paths: string[][]
1633
    ): [substituterNodes, Context, string[][], string] {
1634
      const { left, right } = node
9✔
1635
      if (isIrreducible(left, context)) {
9✔
1636
        if (!(left.type === 'Literal' && typeof left.value === 'boolean')) {
4✔
1637
          throw new rttc.TypeError(left, ' on left hand side of operation', 'boolean', left.type)
2✔
1638
        } else {
1639
          const result =
1640
            node.operator === '&&'
2✔
1641
              ? left.value
2!
1642
                ? right
×
1643
                : ast.literal(false, node.loc)
1644
              : left.value
1645
              ? ast.literal(true, node.loc)
2!
1646
              : right
1647
          return [result as es.Expression, context, paths, explain(node)]
2✔
1648
        }
1649
      } else {
1650
        paths[0].push('left')
5✔
1651
        const [reducedLeft, cont, path, str] = reduce(left, context, paths)
5✔
1652
        return [
5✔
1653
          ast.logicalExpression(
1654
            node.operator,
1655
            reducedLeft as es.Expression,
1656
            right,
1657
            node.loc
1658
          ) as substituterNodes,
1659
          cont,
1660
          path,
1661
          str
1662
        ]
1663
      }
1664
    },
1665

1666
    // core of the subst model
1667
    CallExpression(
1668
      node: es.CallExpression,
1669
      context: Context,
1670
      paths: string[][]
1671
    ): [substituterNodes, Context, string[][], string] {
1672
      const [callee, args] = [node.callee, node.arguments]
1,369✔
1673
      // source 0: discipline: any expression can be transformed into either literal, ident(builtin) or funexp
1674
      // if functor can reduce, reduce functor
1675
      if (!isIrreducible(callee, context)) {
1,369✔
1676
        paths[0].push('callee')
40✔
1677
        const [reducedCallee, cont, path, str] = reduce(callee, context, paths)
40✔
1678
        return [
39✔
1679
          ast.callExpression(reducedCallee as es.Expression, args as es.Expression[], node.loc),
1680
          cont,
1681
          path,
1682
          str
1683
        ]
1684
      } else if (callee.type === 'Literal') {
1,329!
1685
        throw new errors.CallingNonFunctionValue(callee, node)
×
1686
      } else if (
1,329!
1687
        callee.type === 'Identifier' &&
1,508✔
1688
        !(callee.name in context.runtime.environments[0].head)
1689
      ) {
1690
        throw new errors.UndefinedVariable(callee.name, callee)
×
1691
      } else {
1692
        // callee is builtin or funexp
1693
        if (
1,329!
1694
          (callee.type === 'FunctionExpression' || callee.type === 'ArrowFunctionExpression') &&
2,730✔
1695
          args.length !== callee.params.length
1696
        ) {
1697
          throw new errors.InvalidNumberOfArguments(node, args.length, callee.params.length)
×
1698
        } else {
1699
          for (let i = 0; i < args.length; i++) {
1,329✔
1700
            const currentArg = args[i]
1,172✔
1701
            if (!isIrreducible(currentArg, context)) {
1,172✔
1702
              paths[0].push('arguments[' + i + ']')
425✔
1703
              const [reducedCurrentArg, cont, path, str] = reduce(currentArg, context, paths)
425✔
1704
              const reducedArgs = [...args.slice(0, i), reducedCurrentArg, ...args.slice(i + 1)]
425✔
1705
              return [
425✔
1706
                ast.callExpression(
1707
                  callee as es.Expression,
1708
                  reducedArgs as es.Expression[],
1709
                  node.loc
1710
                ),
1711
                cont,
1712
                path,
1713
                str
1714
              ]
1715
            }
1716
            if (
747!
1717
              currentArg.type === 'Identifier' &&
749✔
1718
              !(currentArg.name in context.runtime.environments[0].head)
1719
            ) {
1720
              throw new errors.UndefinedVariable(currentArg.name, currentArg)
×
1721
            }
1722
          }
1723
        }
1724
        // if it reaches here, means all the arguments are legal.
1725
        if (['FunctionExpression', 'ArrowFunctionExpression'].includes(callee.type)) {
904✔
1726
          return [
774✔
1727
            apply(callee as FunctionDeclarationExpression, args as es.Literal[]),
1728
            context,
1729
            paths,
1730
            explain(node)
1731
          ]
1732
        } else {
1733
          if ((callee as es.Identifier).name.includes('math')) {
130✔
1734
            return [
4✔
1735
              builtin.evaluateMath((callee as es.Identifier).name, ...args),
1736
              context,
1737
              paths,
1738
              explain(node)
1739
            ]
1740
          } else if (typeof builtin[(callee as es.Identifier).name] === 'function') {
126✔
1741
            return [builtin[(callee as es.Identifier).name](...args), context, paths, explain(node)]
126✔
1742
          }
1743
          return [
×
1744
            builtin.evaluateModuleFunction((callee as es.Identifier).name, context, ...args),
1745
            context,
1746
            paths,
1747
            explain(node)
1748
          ]
1749
        }
1750
      }
1751
    },
1752

1753
    Program(
1754
      node: es.Program,
1755
      context: Context,
1756
      paths: string[][]
1757
    ): [substituterNodes, Context, string[][], string] {
1758
      if (node.body.length === 0) {
1,956!
1759
        return [ast.expressionStatement(ast.identifier('undefined')), context, paths, explain(node)]
×
1760
      } else {
1761
        const [firstStatement, ...otherStatements] = node.body
1,956✔
1762
        if (firstStatement.type === 'ReturnStatement') {
1,956!
1763
          return [firstStatement, context, paths, explain(node)]
×
1764
        } else if (firstStatement.type === 'IfStatement') {
1,956!
1765
          paths[0].push('body[0]')
×
1766
          const [reduced, cont, path, str] = reduce(firstStatement, context, paths)
×
1767
          if (reduced.type === 'BlockStatement') {
×
1768
            const body = reduced.body as es.Statement[]
×
1769
            if (body.length > 1) {
×
1770
              path[1] = [...path[0].slice(0, path[0].length - 1)]
×
1771
            }
1772
            const wholeBlock = body.concat(...(otherStatements as es.Statement[]))
×
1773
            return [ast.program(wholeBlock), cont, path, str]
×
1774
          } else {
1775
            return [
×
1776
              ast.program([reduced as es.Statement, ...(otherStatements as es.Statement[])]),
1777
              cont,
1778
              path,
1779
              str
1780
            ]
1781
          }
1782
        } else if (
1,956✔
1783
          firstStatement.type === 'ExpressionStatement' &&
3,584✔
1784
          isIrreducible(firstStatement.expression, context)
1785
        ) {
1786
          // let stmt
1787
          // if (otherStatements.length > 0) {
1788
          paths[0].push('body[0]')
55✔
1789
          paths.push([])
55✔
1790
          const stmt = ast.program(otherStatements as es.Statement[])
55✔
1791
          // } else {
1792
          //   stmt = ast.expressionStatement(firstStatement.expression)
1793
          // }
1794
          return [stmt, context, paths, explain(node)]
55✔
1795
        } else if (firstStatement.type === 'FunctionDeclaration') {
1,901✔
1796
          if (firstStatement.id === null) {
259!
1797
            throw new Error(
×
1798
              'Encountered a FunctionDeclaration node without an identifier. This should have been caught when parsing.'
1799
            )
1800
          }
1801
          let funDecExp = ast.functionDeclarationExpression(
259✔
1802
            firstStatement.id,
1803
            firstStatement.params,
1804
            firstStatement.body
1805
          ) as FunctionDeclarationExpression
1806
          // substitute body
1807
          funDecExp = substituteMain(funDecExp.id, funDecExp, funDecExp, [
259✔
1808
            []
1809
          ])[0] as FunctionDeclarationExpression
1810
          // substitute the rest of the program
1811
          const remainingProgram = ast.program(otherStatements as es.Statement[])
259✔
1812
          // substitution within the same program, add " same" so that substituter can differentiate between
1813
          // substitution within the program and substitution from outside the program
1814
          const newId = ast.identifier(funDecExp.id.name + ' same', funDecExp.id.loc)
259✔
1815
          const subst = substituteMain(newId, funDecExp, remainingProgram, paths)
259✔
1816
          // concats paths such that:
1817
          // paths[0] -> path to the program to be substituted, pre-redex
1818
          // paths[1...] -> path(s) to the parts of the remaining program
1819
          // that were substituted, post-redex
1820
          paths[0].push('body[0]')
259✔
1821
          const allPaths = paths.concat(subst[1])
259✔
1822
          if (subst[1].length === 0) {
259✔
1823
            allPaths.push([])
113✔
1824
          }
1825
          return [subst[0], context, allPaths, explain(node)]
259✔
1826
        } else if (firstStatement.type === 'VariableDeclaration') {
1,642✔
1827
          const { kind, declarations } = firstStatement
63✔
1828
          if (kind !== 'const') {
63!
1829
            // TODO: cannot use let or var
1830
            return [dummyProgram(), context, paths, 'cannot use let or var']
×
1831
          } else if (
63!
1832
            declarations.length <= 0 ||
252✔
1833
            declarations.length > 1 ||
1834
            declarations[0].type !== 'VariableDeclarator' ||
1835
            !declarations[0].init
1836
          ) {
1837
            // TODO: syntax error
1838
            return [dummyProgram(), context, paths, 'syntax error']
×
1839
          } else {
1840
            const declarator = declarations[0] as es.VariableDeclarator
63✔
1841
            const rhs = declarator.init!
63✔
1842
            if (declarator.id.type !== 'Identifier') {
63!
1843
              // TODO: source does not allow destructuring
1844
              return [dummyProgram(), context, paths, 'source does not allow destructuring']
×
1845
            } else if (isIrreducible(rhs, context)) {
63!
1846
              const remainingProgram = ast.program(otherStatements as es.Statement[])
63✔
1847
              // force casting for weird errors
1848
              // substitution within the same program, add " same" so that substituter can differentiate between
1849
              // substitution within the program and substitution from outside the program
1850
              const newId = ast.identifier(declarator.id.name + ' same', declarator.id.loc)
63✔
1851
              const subst = substituteMain(
63✔
1852
                newId,
1853
                rhs as es.ArrayExpression,
1854
                remainingProgram,
1855
                paths
1856
              )
1857
              // concats paths such that:
1858
              // paths[0] -> path to the program to be substituted, pre-redex
1859
              // paths[1...] -> path(s) to the parts of the remaining program
1860
              // that were substituted, post-redex
1861
              paths[0].push('body[0]')
63✔
1862
              const allPaths = paths.concat(subst[1])
63✔
1863
              if (subst[1].length === 0) {
63✔
1864
                allPaths.push([])
1✔
1865
              }
1866
              return [subst[0], context, allPaths, explain(node)]
63✔
1867
            } else if (
×
1868
              rhs.type === 'ArrowFunctionExpression' ||
×
1869
              rhs.type === 'FunctionExpression'
1870
            ) {
1871
              let funDecExp = ast.functionDeclarationExpression(
×
1872
                declarator.id,
1873
                rhs.params,
1874
                rhs.body.type === 'BlockStatement'
1875
                  ? rhs.body
×
1876
                  : ast.blockStatement([ast.returnStatement(rhs.body)])
1877
              ) as FunctionDeclarationExpression
1878
              // substitute body
1879
              funDecExp = substituteMain(funDecExp.id, funDecExp, funDecExp, [
×
1880
                []
1881
              ])[0] as FunctionDeclarationExpression
1882
              // substitute the rest of the program
1883
              const remainingProgram = ast.program(otherStatements as es.Statement[])
×
1884
              // substitution within the same block, add " same" so that substituter can differentiate between
1885
              // substitution within the block and substitution from outside the block
1886
              const newId = ast.identifier(funDecExp.id.name + ' same', funDecExp.id.loc)
×
1887
              const subst = substituteMain(newId, funDecExp, remainingProgram, paths)
×
1888
              // concats paths such that:
1889
              // paths[0] -> path to the program to be substituted, pre-redex
1890
              // paths[1...] -> path(s) to the parts of the remaining program
1891
              // that were substituted, post-redex
1892
              paths[0].push('body[0]')
×
1893
              const allPaths = paths.concat(subst[1])
×
1894
              if (subst[1].length === 0) {
×
1895
                allPaths.push([])
×
1896
              }
1897
              return [subst[0], context, allPaths, explain(node)]
×
1898
            } else {
1899
              paths[0].push('body[0]')
×
1900
              paths[0].push('declarations[0]')
×
1901
              paths[0].push('init')
×
1902
              const [reducedRhs, cont, path, str] = reduce(rhs, context, paths)
×
1903
              return [
×
1904
                ast.program([
1905
                  ast.declaration(
1906
                    declarator.id.name,
1907
                    'const',
1908
                    reducedRhs as es.Expression
1909
                  ) as es.Statement,
1910
                  ...(otherStatements as es.Statement[])
1911
                ]),
1912
                cont,
1913
                path,
1914
                str
1915
              ]
1916
            }
1917
          }
1918
        }
1919
        paths[0].push('body[0]')
1,579✔
1920
        const [reduced, cont, path, str] = reduce(firstStatement, context, paths)
1,579✔
1921
        return [
1,572✔
1922
          ast.program([reduced as es.Statement, ...(otherStatements as es.Statement[])]),
1923
          cont,
1924
          path,
1925
          str
1926
        ]
1927
      }
1928
    },
1929

1930
    BlockStatement(
1931
      node: es.BlockStatement,
1932
      context: Context,
1933
      paths: string[][]
1934
    ): [substituterNodes, Context, string[][], string] {
1935
      if (node.body.length === 0) {
7!
1936
        return [ast.expressionStatement(ast.identifier('undefined')), context, paths, explain(node)]
×
1937
      } else {
1938
        const [firstStatement, ...otherStatements] = node.body
7✔
1939
        if (firstStatement.type === 'ReturnStatement') {
7✔
1940
          return [firstStatement, context, paths, explain(node)]
1✔
1941
        } else if (firstStatement.type === 'IfStatement') {
6!
1942
          paths[0].push('body[0]')
×
1943
          const [reduced, cont, path, str] = reduce(firstStatement, context, paths)
×
1944
          if (reduced.type === 'BlockStatement') {
×
1945
            const body = reduced.body as es.Statement[]
×
1946
            if (body.length > 1) {
×
1947
              path[1] = [...path[0].slice(0, path[0].length - 1)]
×
1948
            }
1949
            const wholeBlock = body.concat(...(otherStatements as es.Statement[]))
×
1950
            return [ast.blockStatement(wholeBlock), cont, path, str]
×
1951
          } else {
1952
            return [
×
1953
              ast.blockStatement([reduced as es.Statement, ...(otherStatements as es.Statement[])]),
1954
              cont,
1955
              path,
1956
              str
1957
            ]
1958
          }
1959
        } else if (
6✔
1960
          firstStatement.type === 'ExpressionStatement' &&
9✔
1961
          isIrreducible(firstStatement.expression, context)
1962
        ) {
1963
          let stmt
1964
          if (otherStatements.length > 0) {
1!
1965
            paths[0].push('body[0]')
×
1966
            paths.push([])
×
1967
            stmt = ast.blockStatement(otherStatements as es.Statement[])
×
1968
          } else {
1969
            stmt = ast.expressionStatement(firstStatement.expression)
1✔
1970
          }
1971
          return [stmt, context, paths, explain(node)]
1✔
1972
        } else if (firstStatement.type === 'FunctionDeclaration') {
5!
1973
          let funDecExp = ast.functionDeclarationExpression(
×
1974
            firstStatement.id!,
1975
            firstStatement.params,
1976
            firstStatement.body
1977
          ) as FunctionDeclarationExpression
1978
          // substitute body
1979
          funDecExp = substituteMain(funDecExp.id, funDecExp, funDecExp, [
×
1980
            []
1981
          ])[0] as FunctionDeclarationExpression
1982
          // substitute the rest of the blockStatement
1983
          const remainingBlockStatement = ast.blockStatement(otherStatements as es.Statement[])
×
1984
          // substitution within the same block, add " same" so that substituter can differentiate between
1985
          // substitution within the block and substitution from outside the block
1986
          const newId = ast.identifier(funDecExp.id.name + ' same', funDecExp.id.loc)
×
1987
          const subst = substituteMain(newId, funDecExp, remainingBlockStatement, paths)
×
1988
          // concats paths such that:
1989
          // paths[0] -> path to the program to be substituted, pre-redex
1990
          // paths[1...] -> path(s) to the parts of the remaining program
1991
          // that were substituted, post-redex
1992
          paths[0].push('body[0]')
×
1993
          const allPaths = paths.concat(subst[1])
×
1994
          if (subst[1].length === 0) {
×
1995
            allPaths.push([])
×
1996
          }
1997
          return [subst[0], context, allPaths, explain(node)]
×
1998
        } else if (firstStatement.type === 'VariableDeclaration') {
5✔
1999
          const { kind, declarations } = firstStatement
3✔
2000
          if (kind !== 'const') {
3!
2001
            // TODO: cannot use let or var
2002
            return [dummyBlockStatement(), context, paths, 'cannot use let or var']
×
2003
          } else if (
3!
2004
            declarations.length <= 0 ||
12✔
2005
            declarations.length > 1 ||
2006
            declarations[0].type !== 'VariableDeclarator' ||
2007
            !declarations[0].init
2008
          ) {
2009
            // TODO: syntax error
2010
            return [dummyBlockStatement(), context, paths, 'syntax error']
×
2011
          } else {
2012
            const declarator = declarations[0] as es.VariableDeclarator
3✔
2013
            const rhs = declarator.init!
3✔
2014
            if (declarator.id.type !== 'Identifier') {
3!
2015
              // TODO: source does not allow destructuring
2016
              return [dummyBlockStatement(), context, paths, 'source does not allow destructuring']
×
2017
            } else if (isIrreducible(rhs, context)) {
3!
2018
              const remainingBlockStatement = ast.blockStatement(otherStatements as es.Statement[])
3✔
2019
              // force casting for weird errors
2020
              // substitution within the same block, add " same" so that substituter can differentiate between
2021
              // substitution within the block and substitution from outside the block
2022
              const newId = ast.identifier(declarator.id.name + ' same', declarator.id.loc)
3✔
2023
              const subst = substituteMain(
3✔
2024
                newId,
2025
                rhs as es.ArrayExpression,
2026
                remainingBlockStatement,
2027
                paths
2028
              )
2029
              // concats paths such that:
2030
              // paths[0] -> path to the program to be substituted, pre-redex
2031
              // paths[1...] -> path(s) to the parts of the remaining program
2032
              // that were substituted, post-redex
2033
              paths[0].push('body[0]')
3✔
2034
              const allPaths = paths.concat(subst[1])
3✔
2035
              if (subst[1].length === 0) {
3!
2036
                allPaths.push([])
×
2037
              }
2038
              return [subst[0], context, allPaths, explain(node)]
3✔
2039
            } else if (
×
2040
              rhs.type === 'ArrowFunctionExpression' ||
×
2041
              rhs.type === 'FunctionExpression'
2042
            ) {
2043
              let funDecExp = ast.functionDeclarationExpression(
×
2044
                declarator.id,
2045
                rhs.params,
2046
                rhs.body.type === 'BlockStatement'
2047
                  ? rhs.body
×
2048
                  : ast.blockStatement([ast.returnStatement(rhs.body)])
2049
              ) as FunctionDeclarationExpression
2050
              // substitute body
2051
              funDecExp = substituteMain(funDecExp.id, funDecExp, funDecExp, [
×
2052
                []
2053
              ])[0] as FunctionDeclarationExpression
2054
              // substitute the rest of the blockStatement
2055
              const remainingBlockStatement = ast.blockStatement(otherStatements as es.Statement[])
×
2056
              // substitution within the same block, add " same" so that substituter can differentiate between
2057
              // substitution within the block and substitution from outside the block
2058
              const newId = ast.identifier(funDecExp.id.name + ' same', funDecExp.id.loc)
×
2059
              const subst = substituteMain(newId, funDecExp, remainingBlockStatement, paths)
×
2060
              // concats paths such that:
2061
              // paths[0] -> path to the program to be substituted, pre-redex
2062
              // paths[1...] -> path(s) to the parts of the remaining program
2063
              // that were substituted, post-redex
2064
              paths[0].push('body[0]')
×
2065
              const allPaths = paths.concat(subst[1])
×
2066
              if (subst[1].length === 0) {
×
2067
                allPaths.push([])
×
2068
              }
2069
              return [subst[0], context, allPaths, explain(node)]
×
2070
            } else {
2071
              paths[0].push('body[0]')
×
2072
              paths[0].push('declarations[0]')
×
2073
              paths[0].push('init')
×
2074
              const [reducedRhs, cont, path, str] = reduce(rhs, context, paths)
×
2075
              return [
×
2076
                ast.blockStatement([
2077
                  ast.declaration(
2078
                    declarator.id.name,
2079
                    'const',
2080
                    reducedRhs as es.Expression
2081
                  ) as es.Statement,
2082
                  ...(otherStatements as es.Statement[])
2083
                ]),
2084
                cont,
2085
                path,
2086
                str
2087
              ]
2088
            }
2089
          }
2090
        }
2091
        paths[0].push('body[0]')
2✔
2092
        const [reduced, cont, path, str] = reduce(firstStatement, context, paths)
2✔
2093
        return [
2✔
2094
          ast.blockStatement([reduced as es.Statement, ...(otherStatements as es.Statement[])]),
2095
          cont,
2096
          path,
2097
          str
2098
        ]
2099
      }
2100
    },
2101

2102
    BlockExpression(
2103
      node: BlockExpression,
2104
      context: Context,
2105
      paths: string[][]
2106
    ): [substituterNodes, Context, string[][], string] {
2107
      if (node.body.length === 0) {
277✔
2108
        return [ast.identifier('undefined'), context, paths, explain(node)]
2✔
2109
      } else {
2110
        const [firstStatement, ...otherStatements] = node.body
275✔
2111
        if (firstStatement.type === 'ReturnStatement') {
275✔
2112
          const arg = firstStatement.argument as es.Expression
33✔
2113
          return [arg, context, paths, explain(node)]
33✔
2114
        } else if (firstStatement.type === 'IfStatement') {
242✔
2115
          paths[0].push('body[0]')
28✔
2116
          const [reduced, cont, path, str] = reduce(firstStatement, context, paths)
28✔
2117
          if (reduced.type === 'BlockStatement') {
28✔
2118
            const body = reduced.body as es.Statement[]
11✔
2119
            if (body.length > 1) {
11✔
2120
              path[1] = [...path[0].slice(0, path[0].length - 1)]
4✔
2121
            }
2122
            const wholeBlock = body.concat(...(otherStatements as es.Statement[]))
11✔
2123
            return [ast.blockExpression(wholeBlock), cont, path, str]
11✔
2124
          } else {
2125
            return [
17✔
2126
              ast.blockExpression([
2127
                reduced as es.Statement,
2128
                ...(otherStatements as es.Statement[])
2129
              ]),
2130
              cont,
2131
              path,
2132
              str
2133
            ]
2134
          }
2135
        } else if (
214✔
2136
          firstStatement.type === 'ExpressionStatement' &&
218✔
2137
          isIrreducible(firstStatement.expression, context)
2138
        ) {
2139
          let stmt
2140
          if (otherStatements.length > 0) {
2!
2141
            paths[0].push('body[0]')
×
2142
            paths.push([])
×
2143
            stmt = ast.blockExpression(otherStatements as es.Statement[])
×
2144
          } else {
2145
            stmt = ast.identifier('undefined')
2✔
2146
          }
2147
          return [stmt, context, paths, explain(node)]
2✔
2148
        } else if (firstStatement.type === 'FunctionDeclaration') {
212✔
2149
          let funDecExp = ast.functionDeclarationExpression(
18✔
2150
            firstStatement.id!,
2151
            firstStatement.params,
2152
            firstStatement.body
2153
          ) as FunctionDeclarationExpression
2154
          // substitute body
2155
          funDecExp = substituteMain(funDecExp.id, funDecExp, funDecExp, [
18✔
2156
            []
2157
          ])[0] as FunctionDeclarationExpression
2158
          // substitute the rest of the blockExpression
2159
          const remainingBlockExpression = ast.blockExpression(otherStatements as es.Statement[])
18✔
2160
          // substitution within the same block, add " same" so that substituter can differentiate between
2161
          // substitution within the block and substitution from outside the block
2162
          const newId = ast.identifier(funDecExp.id.name + ' same', funDecExp.id.loc)
18✔
2163
          const subst = substituteMain(newId, funDecExp, remainingBlockExpression, paths)
18✔
2164
          // concats paths such that:
2165
          // paths[0] -> path to the program to be substituted, pre-redex
2166
          // paths[1...] -> path(s) to the parts of the remaining program
2167
          // that were substituted, post-redex
2168
          paths[0].push('body[0]')
18✔
2169
          const allPaths = paths.concat(subst[1])
18✔
2170
          if (subst[1].length === 0) {
18✔
2171
            allPaths.push([])
1✔
2172
          }
2173
          return [subst[0], context, allPaths, explain(node)]
18✔
2174
        } else if (firstStatement.type === 'VariableDeclaration') {
194✔
2175
          const { kind, declarations } = firstStatement
191✔
2176
          if (kind !== 'const') {
191!
2177
            // TODO: cannot use let or var
2178
            return [dummyBlockExpression(), context, paths, 'cannot use let or var']
×
2179
          } else if (
191!
2180
            declarations.length <= 0 ||
764✔
2181
            declarations.length > 1 ||
2182
            declarations[0].type !== 'VariableDeclarator' ||
2183
            !declarations[0].init
2184
          ) {
2185
            // TODO: syntax error
2186
            return [dummyBlockExpression(), context, paths, 'syntax error']
×
2187
          } else {
2188
            const declarator = declarations[0] as es.VariableDeclarator
191✔
2189
            const rhs = declarator.init!
191✔
2190
            if (declarator.id.type !== 'Identifier') {
191!
2191
              // TODO: source does not allow destructuring
2192
              return [dummyBlockExpression(), context, paths, 'source does not allow destructuring']
×
2193
            } else if (isIrreducible(rhs, context)) {
191✔
2194
              const remainingBlockExpression = ast.blockExpression(
12✔
2195
                otherStatements as es.Statement[]
2196
              )
2197
              // forced casting for some weird errors
2198
              // substitution within the same block, add " same" so that substituter can differentiate between
2199
              // substitution within the block and substitution from outside the block
2200
              const newId = ast.identifier(declarator.id.name + ' same', declarator.id.loc)
12✔
2201
              const subst = substituteMain(
12✔
2202
                newId,
2203
                rhs as es.ArrayExpression,
2204
                remainingBlockExpression,
2205
                paths
2206
              )
2207
              // concats paths such that:
2208
              // paths[0] -> path to the program to be substituted, pre-redex
2209
              // paths[1...] -> path(s) to the parts of the remaining program
2210
              // that were substituted, post-redex
2211
              paths[0].push('body[0]')
12✔
2212
              const allPaths = paths.concat(subst[1])
12✔
2213
              if (subst[1].length === 0) {
12✔
2214
                allPaths.push([])
2✔
2215
              }
2216
              return [subst[0], context, allPaths, explain(node)]
12✔
2217
            } else if (
179!
2218
              rhs.type === 'ArrowFunctionExpression' ||
358✔
2219
              rhs.type === 'FunctionExpression'
2220
            ) {
2221
              let funDecExp = ast.functionDeclarationExpression(
×
2222
                declarator.id,
2223
                rhs.params,
2224
                rhs.body.type === 'BlockStatement'
2225
                  ? rhs.body
×
2226
                  : ast.blockStatement([ast.returnStatement(rhs.body)])
2227
              ) as FunctionDeclarationExpression
2228
              // substitute body
2229
              funDecExp = substituteMain(funDecExp.id, funDecExp, funDecExp, [
×
2230
                []
2231
              ])[0] as FunctionDeclarationExpression
2232
              // substitute the rest of the blockExpression
2233
              const remainingBlockExpression = ast.blockExpression(
×
2234
                otherStatements as es.Statement[]
2235
              )
2236
              // substitution within the same block, add " same" so that substituter can differentiate between
2237
              // substitution within the block and substitution from outside the block
2238
              const newId = ast.identifier(funDecExp.id.name + ' same', funDecExp.id.loc)
×
2239
              const subst = substituteMain(newId, funDecExp, remainingBlockExpression, paths)
×
2240
              // concats paths such that:
2241
              // paths[0] -> path to the program to be substituted, pre-redex
2242
              // paths[1...] -> path(s) to the parts of the remaining program
2243
              // that were substituted, post-redex
2244
              paths[0].push('body[0]')
×
2245
              const allPaths = paths.concat(subst[1])
×
2246
              if (subst[1].length === 0) {
×
2247
                allPaths.push([])
×
2248
              }
2249
              return [subst[0], context, allPaths, explain(node)]
×
2250
            } else {
2251
              paths[0].push('body[0]')
179✔
2252
              paths[0].push('declarations[0]')
179✔
2253
              paths[0].push('init')
179✔
2254
              const [reducedRhs, cont, path, str] = reduce(rhs, context, paths)
179✔
2255
              return [
179✔
2256
                ast.blockExpression([
2257
                  ast.declaration(
2258
                    declarator.id.name,
2259
                    'const',
2260
                    reducedRhs as es.Expression
2261
                  ) as es.Statement,
2262
                  ...(otherStatements as es.Statement[])
2263
                ]),
2264
                cont,
2265
                path,
2266
                str
2267
              ]
2268
            }
2269
          }
2270
        }
2271
        paths[0].push('body[0]')
3✔
2272
        const [reduced, cont, path, str] = reduce(firstStatement, context, paths)
3✔
2273
        return [
3✔
2274
          ast.blockExpression([reduced as es.Statement, ...(otherStatements as es.Statement[])]),
2275
          cont,
2276
          path,
2277
          str
2278
        ]
2279
      }
2280
    },
2281

2282
    // source 1
2283
    IfStatement(
2284
      node: es.IfStatement,
2285
      context: Context,
2286
      paths: string[][]
2287
    ): [substituterNodes, Context, string[][], string] {
2288
      const { test, consequent, alternate } = node
28✔
2289
      if (test.type === 'Literal') {
28✔
2290
        const error = rttc.checkIfStatement(node, test.value, context.chapter)
11✔
2291
        if (error === undefined) {
11!
2292
          return [
11✔
2293
            (test.value ? consequent : alternate) as es.Statement,
11✔
2294
            context,
2295
            paths,
2296
            explain(node)
2297
          ]
2298
        } else {
2299
          throw error
×
2300
        }
2301
      } else {
2302
        paths[0].push('test')
17✔
2303
        const [reducedTest, cont, path, str] = reduce(test, context, paths)
17✔
2304
        const reducedIfStatement = ast.ifStatement(
17✔
2305
          reducedTest as es.Expression,
2306
          consequent as es.BlockStatement,
2307
          alternate as es.IfStatement | es.BlockStatement,
2308
          node.loc
2309
        )
2310
        return [reducedIfStatement, cont, path, str]
17✔
2311
      }
2312
    }
2313
  }
2314

2315
  /**
2316
   * Reduces one step of the program and returns
2317
   * 1. The reduced program
2318
   * 2. The path(s) leading to the redex
2319
   *    - If substitution not involved, returns array containing one path
2320
   *    - If substitution is involved, returns array containing
2321
   *      path to program to be substituted pre-redex, as well as
2322
   *      path(s) to the parts of the program that were substituted post-redex
2323
   * 3. String explaining the reduction
2324
   */
2325
  function reduce(
2326
    node: substituterNodes,
2327
    context: Context,
2328
    paths: string[][]
2329
  ): [substituterNodes, Context, string[][], string] {
2330
    const reducer = reducers[node.type]
30,840✔
2331
    if (reducer === undefined) {
30,840!
2332
      return [ast.program([]), context, [], 'error'] // exit early
×
2333
    } else {
2334
      return reducer(node, context, paths)
30,840✔
2335
    }
2336
  }
2337
  return reduce(node, context, [[]])
1,956✔
2338
}
2339

2340
// Main creates a scope for us to control the verbosity
2341
function treeifyMain(target: substituterNodes): substituterNodes {
2342
  // recurse down the program like substitute
2343
  // if see a function at expression position,
2344
  //   has an identifier: replace with the name
2345
  //   else: replace with an identifer "=>"
2346
  let verboseCount = 0
3,528✔
2347
  const treeifiers = {
3,528✔
2348
    // Identifier: return
2349
    ExpressionStatement: (target: es.ExpressionStatement): es.ExpressionStatement => {
2350
      return ast.expressionStatement(treeify(target.expression) as es.Expression)
3,609✔
2351
    },
2352

2353
    BinaryExpression: (target: es.BinaryExpression) => {
2354
      return ast.binaryExpression(
52,600✔
2355
        target.operator,
2356
        treeify(target.left) as es.Expression,
2357
        treeify(target.right) as es.Expression
2358
      )
2359
    },
2360

2361
    UnaryExpression: (target: es.UnaryExpression): es.UnaryExpression => {
2362
      return ast.unaryExpression(target.operator, treeify(target.argument) as es.Expression)
32✔
2363
    },
2364

2365
    ConditionalExpression: (target: es.ConditionalExpression): es.ConditionalExpression => {
2366
      return ast.conditionalExpression(
748✔
2367
        treeify(target.test) as es.Expression,
2368
        treeify(target.consequent) as es.Expression,
2369
        treeify(target.alternate) as es.Expression
2370
      )
2371
    },
2372

2373
    LogicalExpression: (target: es.LogicalExpression) => {
2374
      return ast.logicalExpression(
56✔
2375
        target.operator,
2376
        treeify(target.left) as es.Expression,
2377
        treeify(target.right) as es.Expression
2378
      )
2379
    },
2380

2381
    CallExpression: (target: es.CallExpression): es.CallExpression => {
2382
      return ast.callExpression(
7,560✔
2383
        treeify(target.callee) as es.Expression,
2384
        target.arguments.map(arg => treeify(arg) as es.Expression)
9,594✔
2385
      )
2386
    },
2387

2388
    FunctionDeclaration: (target: es.FunctionDeclaration): es.FunctionDeclaration => {
2389
      return ast.functionDeclaration(
206✔
2390
        target.id,
2391
        target.params,
2392
        treeify(target.body) as es.BlockStatement
2393
      )
2394
    },
2395

2396
    // CORE
2397
    FunctionExpression: (
2398
      target: es.FunctionExpression
2399
    ): es.Identifier | es.ArrowFunctionExpression => {
2400
      if (target.id) {
4,159!
2401
        return target.id
4,159✔
2402
      } else if (verboseCount < 5) {
×
2403
        // here onwards is guarding against arrow turned function expressions
2404
        verboseCount++
×
2405
        const redacted = ast.arrowFunctionExpression(
×
2406
          target.params,
2407
          treeify(target.body) as es.BlockStatement
2408
        )
2409
        verboseCount = 0
×
2410
        return redacted
×
2411
      } else {
2412
        // shortens body after 5 iterations
2413
        return ast.arrowFunctionExpression(target.params, ast.identifier('...'))
×
2414
      }
2415
    },
2416

2417
    Program: (target: es.Program): es.Program => {
2418
      return ast.program(target.body.map(stmt => treeify(stmt) as es.Statement))
4,061✔
2419
    },
2420

2421
    BlockStatement: (target: es.BlockStatement): es.BlockStatement => {
2422
      return ast.blockStatement(target.body.map(stmt => treeify(stmt) as es.Statement))
580✔
2423
    },
2424

2425
    BlockExpression: (target: BlockExpression): es.BlockStatement => {
2426
      return ast.blockStatement(target.body.map(treeify) as es.Statement[])
554✔
2427
    },
2428

2429
    ReturnStatement: (target: es.ReturnStatement): es.ReturnStatement => {
2430
      return ast.returnStatement(treeify(target.argument!) as es.Expression)
850✔
2431
    },
2432

2433
    // source 1
2434
    // CORE
2435
    ArrowFunctionExpression: (
2436
      target: es.ArrowFunctionExpression
2437
    ): es.Identifier | es.ArrowFunctionExpression => {
2438
      if (verboseCount < 5) {
1,457✔
2439
        // here onwards is guarding against arrow turned function expressions
2440
        verboseCount++
1,438✔
2441
        const redacted = ast.arrowFunctionExpression(
1,438✔
2442
          target.params,
2443
          treeify(target.body) as es.BlockStatement
2444
        )
2445
        verboseCount = 0
1,438✔
2446
        return redacted
1,438✔
2447
      } else {
2448
        // shortens body after 5 iterations
2449
        return ast.arrowFunctionExpression(target.params, ast.identifier('...'))
19✔
2450
      }
2451
    },
2452

2453
    VariableDeclaration: (target: es.VariableDeclaration): es.VariableDeclaration => {
2454
      return ast.variableDeclaration(target.declarations.map(treeify) as es.VariableDeclarator[])
850✔
2455
    },
2456

2457
    VariableDeclarator: (target: es.VariableDeclarator): es.VariableDeclarator => {
2458
      return ast.variableDeclarator(target.id, treeify(target.init!) as es.Expression)
850✔
2459
    },
2460

2461
    IfStatement: (target: es.IfStatement): es.IfStatement => {
2462
      return ast.ifStatement(
82✔
2463
        treeify(target.test) as es.Expression,
2464
        treeify(target.consequent) as es.BlockStatement,
2465
        treeify(target.alternate!) as es.BlockStatement | es.IfStatement
2466
      )
2467
    },
2468

2469
    // source 2
2470
    ArrayExpression: (target: es.ArrayExpression): es.ArrayExpression => {
2471
      return ast.arrayExpression(
8,993✔
2472
        (target.elements as ContiguousArrayElements).map(treeify) as es.Expression[]
2473
      )
2474
    }
2475
  }
2476

2477
  function treeify(target: substituterNodes): substituterNodes {
2478
    const treeifier = treeifiers[target.type]
159,918✔
2479
    if (treeifier === undefined) {
159,918✔
2480
      return target
73,352✔
2481
    } else {
2482
      return treeifier(target)
86,566✔
2483
    }
2484
  }
2485

2486
  return treeify(target)
3,528✔
2487
}
2488

2489
function jsTreeifyMain(
2490
  target: substituterNodes,
2491
  visited: Set<substituterNodes>,
2492
  readOnly: boolean
2493
): substituterNodes {
2494
  // recurse down the program like substitute
2495
  // if see a function at expression position,
2496
  //   visited before recursing to this target: replace with the name
2497
  //   else: replace with a FunctionExpression
2498
  let verboseCount = 0
5✔
2499
  const treeifiers = {
5✔
2500
    Identifier: (target: es.Identifier): es.Identifier => {
2501
      if (readOnly && target.name.startsWith('anonymous_')) {
1!
2502
        return ast.identifier('[Function]')
×
2503
      }
2504
      return target
1✔
2505
    },
2506

2507
    Literal: (target: es.Literal): es.Literal => {
2508
      if (typeof target.value === 'object') {
4!
2509
        target.raw = objectToString(target.value)
×
2510
      }
2511
      return target
4✔
2512
    },
2513

2514
    ExpressionStatement: (target: es.ExpressionStatement): es.ExpressionStatement => {
2515
      return ast.expressionStatement(treeify(target.expression) as es.Expression)
×
2516
    },
2517

2518
    BinaryExpression: (target: es.BinaryExpression) => {
2519
      return ast.binaryExpression(
×
2520
        target.operator,
2521
        treeify(target.left) as es.Expression,
2522
        treeify(target.right) as es.Expression
2523
      )
2524
    },
2525

2526
    UnaryExpression: (target: es.UnaryExpression): es.UnaryExpression => {
2527
      return ast.unaryExpression(target.operator, treeify(target.argument) as es.Expression)
4✔
2528
    },
2529

2530
    ConditionalExpression: (target: es.ConditionalExpression): es.ConditionalExpression => {
2531
      return ast.conditionalExpression(
×
2532
        treeify(target.test) as es.Expression,
2533
        treeify(target.consequent) as es.Expression,
2534
        treeify(target.alternate) as es.Expression
2535
      )
2536
    },
2537

2538
    LogicalExpression: (target: es.LogicalExpression) => {
2539
      return ast.logicalExpression(
×
2540
        target.operator,
2541
        treeify(target.left) as es.Expression,
2542
        treeify(target.right) as es.Expression
2543
      )
2544
    },
2545

2546
    CallExpression: (target: es.CallExpression): es.CallExpression => {
2547
      return ast.callExpression(
×
2548
        treeify(target.callee) as es.Expression,
2549
        target.arguments.map(arg => treeify(arg) as es.Expression)
×
2550
      )
2551
    },
2552

2553
    FunctionDeclaration: (target: es.FunctionDeclaration): es.FunctionDeclaration => {
2554
      return ast.functionDeclaration(
×
2555
        target.id,
2556
        target.params,
2557
        treeify(target.body) as es.BlockStatement
2558
      )
2559
    },
2560

2561
    // CORE
2562
    FunctionExpression: (target: es.FunctionExpression): es.Identifier | es.FunctionExpression => {
2563
      if (visited.has(target) && target.id) {
×
2564
        return target.id
×
2565
      }
2566
      visited.add(target)
×
2567
      if (readOnly && target.id) {
×
2568
        return target.id
×
2569
      } else if (target.id) {
×
2570
        return ast.functionExpression(
×
2571
          target.params,
2572
          treeify(target.body) as es.BlockStatement,
2573
          target.loc,
2574
          target.id
2575
        )
2576
      } else {
2577
        return ast.functionExpression(
×
2578
          target.params,
2579
          treeify(target.body) as es.BlockStatement,
2580
          target.loc
2581
        )
2582
      }
2583
    },
2584

2585
    Program: (target: es.Program): es.Program => {
2586
      return ast.program(target.body.map(stmt => treeify(stmt) as es.Statement))
×
2587
    },
2588

2589
    BlockStatement: (target: es.BlockStatement): es.BlockStatement => {
2590
      return ast.blockStatement(target.body.map(stmt => treeify(stmt) as es.Statement))
×
2591
    },
2592

2593
    BlockExpression: (target: BlockExpression): es.BlockStatement => {
2594
      return ast.blockStatement(target.body.map(node => treeify(node)) as es.Statement[])
×
2595
    },
2596

2597
    ReturnStatement: (target: es.ReturnStatement): es.ReturnStatement => {
2598
      return ast.returnStatement(treeify(target.argument!) as es.Expression)
×
2599
    },
2600

2601
    // source 1
2602
    ArrowFunctionExpression: (
2603
      target: es.ArrowFunctionExpression
2604
    ): es.Identifier | es.ArrowFunctionExpression => {
2605
      if (verboseCount < 5) {
×
2606
        // here onwards is guarding against arrow turned function expressions
2607
        verboseCount++
×
2608
        const redacted = ast.arrowFunctionExpression(
×
2609
          target.params,
2610
          treeify(target.body) as es.BlockStatement
2611
        )
2612
        verboseCount = 0
×
2613
        return redacted
×
2614
      } else {
2615
        // shortens body after 5 iterations
2616
        return ast.arrowFunctionExpression(target.params, ast.identifier('...'))
×
2617
      }
2618
    },
2619

2620
    VariableDeclaration: (target: es.VariableDeclaration): es.VariableDeclaration => {
2621
      return ast.variableDeclaration(target.declarations.map(treeify) as es.VariableDeclarator[])
×
2622
    },
2623

2624
    VariableDeclarator: (target: es.VariableDeclarator): es.VariableDeclarator => {
2625
      return ast.variableDeclarator(target.id, treeify(target.init!) as es.Expression)
×
2626
    },
2627

2628
    IfStatement: (target: es.IfStatement): es.IfStatement => {
2629
      return ast.ifStatement(
×
2630
        treeify(target.test) as es.Expression,
2631
        treeify(target.consequent) as es.BlockStatement,
2632
        treeify(target.alternate!) as es.BlockStatement | es.IfStatement
2633
      )
2634
    },
2635

2636
    // source 2
2637
    ArrayExpression: (target: es.ArrayExpression): es.ArrayExpression => {
2638
      return ast.arrayExpression(
×
2639
        (target.elements as ContiguousArrayElements).map(treeify) as es.Expression[]
2640
      )
2641
    }
2642
  }
2643

2644
  function treeify(target: substituterNodes): substituterNodes {
2645
    const treeifier = treeifiers[target.type]
9✔
2646
    if (treeifier === undefined) {
9!
2647
      return target
×
2648
    } else {
2649
      return treeifier(target)
9✔
2650
    }
2651
  }
2652

2653
  return treeify(target)
5✔
2654
}
2655

2656
// Mainly kept for testing
2657
export const codify = (node: substituterNodes): string => generate(treeifyMain(node))
3,528✔
2658

2659
export const javascriptify = (node: substituterNodes): string =>
67✔
2660
  '(' + generate(jsTreeifyMain(node, new Set(), false)) + ');'
5✔
2661

2662
/**
2663
 * Recurses down the tree, tracing path to redex
2664
 * and calling treeifyMain on all other children
2665
 * Once redex is found, extract redex from tree
2666
 * and put redexMarker in its place
2667
 * Returns array containing modified tree and
2668
 * extracted redex
2669
 */
2670
function pathifyMain(
2671
  target: substituterNodes,
2672
  paths: string[][],
2673
  visited: Set<substituterNodes>
2674
): [substituterNodes, substituterNodes] {
2675
  let pathIndex = 0
×
2676
  let path = paths[0]
×
2677
  let redex = ast.program([]) as substituterNodes
×
2678
  let endIndex = path === undefined ? 0 : path.length - 1
×
2679
  const redexMarker = ast.identifier('@redex') as substituterNodes
×
2680
  const withBrackets = ast.identifier('(@redex)') as substituterNodes
×
2681
  const pathifiers = {
×
2682
    ExpressionStatement: (target: es.ExpressionStatement): es.ExpressionStatement => {
2683
      let exp = jsTreeifyMain(target.expression, visited, true) as es.Expression
×
2684
      if (path[pathIndex] === 'expression') {
×
2685
        if (pathIndex === endIndex) {
×
2686
          redex = exp
×
2687
          exp =
×
2688
            target.expression.type === 'ArrowFunctionExpression'
2689
              ? (withBrackets as es.Expression)
×
2690
              : (redexMarker as es.Expression)
2691
        } else {
2692
          pathIndex++
×
2693
          exp = pathify(target.expression) as es.Expression
×
2694
        }
2695
      }
2696
      return ast.expressionStatement(exp)
×
2697
    },
2698

2699
    BinaryExpression: (target: es.BinaryExpression) => {
2700
      let left = jsTreeifyMain(target.left, visited, true) as es.Expression
×
2701
      let right = jsTreeifyMain(target.right, visited, true) as es.Expression
×
2702
      if (path[pathIndex] === 'left') {
×
2703
        if (pathIndex === endIndex) {
×
2704
          redex = left
×
2705
          if (redex.type === 'ConditionalExpression') {
×
2706
            left = withBrackets as es.Expression
×
2707
          } else {
2708
            left = redexMarker as es.Expression
×
2709
          }
2710
        } else {
2711
          pathIndex++
×
2712
          left = pathify(target.left) as es.Expression
×
2713
        }
2714
      } else if (path[pathIndex] === 'right') {
×
2715
        if (pathIndex === endIndex) {
×
2716
          redex = right
×
2717
          if (redex.type === 'BinaryExpression' || redex.type === 'ConditionalExpression') {
×
2718
            right = withBrackets as es.Expression
×
2719
          } else {
2720
            right = redexMarker as es.Expression
×
2721
          }
2722
        } else {
2723
          pathIndex++
×
2724
          right = pathify(target.right) as es.Expression
×
2725
        }
2726
      }
2727
      return ast.binaryExpression(target.operator, left, right)
×
2728
    },
2729

2730
    UnaryExpression: (target: es.UnaryExpression): es.UnaryExpression => {
2731
      let arg = jsTreeifyMain(target.argument, visited, true) as es.Expression
×
2732
      if (path[pathIndex] === 'argument') {
×
2733
        if (pathIndex === endIndex) {
×
2734
          redex = arg
×
2735
          arg = redexMarker as es.Expression
×
2736
        } else {
2737
          pathIndex++
×
2738
          arg = pathify(target.argument) as es.Expression
×
2739
        }
2740
      }
2741
      return ast.unaryExpression(target.operator, arg)
×
2742
    },
2743

2744
    ConditionalExpression: (target: es.ConditionalExpression): es.ConditionalExpression => {
2745
      let test = jsTreeifyMain(target.test, visited, true) as es.Expression
×
2746
      let cons = jsTreeifyMain(target.consequent, visited, true) as es.Expression
×
2747
      let alt = jsTreeifyMain(target.alternate, visited, true) as es.Expression
×
2748
      if (path[pathIndex] === 'test') {
×
2749
        if (pathIndex === endIndex) {
×
2750
          redex = test
×
2751
          test = redexMarker as es.Expression
×
2752
        } else {
2753
          pathIndex++
×
2754
          test = pathify(target.test) as es.Expression
×
2755
        }
2756
      } else if (path[pathIndex] === 'consequent') {
×
2757
        if (pathIndex === endIndex) {
×
2758
          redex = cons
×
2759
          cons = redexMarker as es.Expression
×
2760
        } else {
2761
          pathIndex++
×
2762
          cons = pathify(target.consequent) as es.Expression
×
2763
        }
2764
      } else if (path[pathIndex] === 'alternate') {
×
2765
        if (pathIndex === endIndex) {
×
2766
          redex = alt
×
2767
          alt = redexMarker as es.Expression
×
2768
        } else {
2769
          pathIndex++
×
2770
          alt = pathify(target.alternate) as es.Expression
×
2771
        }
2772
      }
2773
      return ast.conditionalExpression(test, cons, alt)
×
2774
    },
2775

2776
    LogicalExpression: (target: es.LogicalExpression) => {
2777
      let left = jsTreeifyMain(target.left, visited, true) as es.Expression
×
2778
      let right = jsTreeifyMain(target.right, visited, true) as es.Expression
×
2779
      if (path[pathIndex] === 'left') {
×
2780
        if (pathIndex === endIndex) {
×
2781
          redex = left
×
2782
          left = redexMarker as es.Expression
×
2783
        } else {
2784
          pathIndex++
×
2785
          left = pathify(target.left) as es.Expression
×
2786
        }
2787
      } else if (path[pathIndex] === 'right') {
×
2788
        if (pathIndex === endIndex) {
×
2789
          redex = right
×
2790
          right = redexMarker as es.Expression
×
2791
        } else {
2792
          pathIndex++
×
2793
          right = pathify(target.right) as es.Expression
×
2794
        }
2795
      }
2796
      return ast.logicalExpression(target.operator, left, right)
×
2797
    },
2798

2799
    CallExpression: (target: es.CallExpression): es.CallExpression => {
2800
      let callee = jsTreeifyMain(target.callee, visited, true) as es.Expression
×
2801
      const args = target.arguments.map(arg => jsTreeifyMain(arg, visited, true) as es.Expression)
×
2802
      if (path[pathIndex] === 'callee') {
×
2803
        if (pathIndex === endIndex) {
×
2804
          redex = callee
×
2805
          callee =
×
2806
            target.callee.type === 'ArrowFunctionExpression'
2807
              ? (withBrackets as es.Expression)
×
2808
              : (redexMarker as es.Expression)
2809
        } else {
2810
          pathIndex++
×
2811
          callee = pathify(target.callee) as es.Expression
×
2812
        }
2813
      } else {
2814
        let argIndex
2815
        const isEnd = pathIndex === endIndex
×
2816
        for (let i = 0; i < target.arguments.length; i++) {
×
2817
          if (path[pathIndex] === 'arguments[' + i + ']') {
×
2818
            argIndex = i
×
2819
            break
×
2820
          }
2821
        }
2822
        if (argIndex !== undefined) {
×
2823
          pathIndex++
×
2824
          if (isEnd) {
×
2825
            redex = args[argIndex]
×
2826
            args[argIndex] = redexMarker as es.Expression
×
2827
          } else {
2828
            args[argIndex] = pathify(target.arguments[argIndex]) as es.Expression
×
2829
          }
2830
        }
2831
      }
2832
      return ast.callExpression(callee, args)
×
2833
    },
2834

2835
    FunctionDeclaration: (target: es.FunctionDeclaration): es.FunctionDeclaration => {
2836
      let body = jsTreeifyMain(target.body, visited, true) as es.BlockStatement
×
2837
      if (path[pathIndex] === 'body') {
×
2838
        if (pathIndex === endIndex) {
×
2839
          redex = body
×
2840
          body = redexMarker as es.BlockStatement
×
2841
        } else {
2842
          pathIndex++
×
2843
          body = pathify(target.body) as es.BlockStatement
×
2844
        }
2845
      }
2846
      return ast.functionDeclaration(target.id, target.params, body)
×
2847
    },
2848

2849
    FunctionExpression: (
2850
      target: es.FunctionExpression
2851
    ): es.Identifier | es.ArrowFunctionExpression => {
2852
      if (target.id) {
×
2853
        return target.id
×
2854
      } else {
2855
        let body = jsTreeifyMain(target.body, visited, true) as es.BlockStatement
×
2856
        if (path[pathIndex] === 'body') {
×
2857
          if (pathIndex === endIndex) {
×
2858
            redex = body
×
2859
            body = redexMarker as es.BlockStatement
×
2860
          } else {
2861
            pathIndex++
×
2862
            body = pathify(target.body) as es.BlockStatement
×
2863
          }
2864
        }
2865
        return ast.arrowFunctionExpression(target.params, body)
×
2866
      }
2867
    },
2868

2869
    Program: (target: es.Program): es.Program => {
2870
      const body = target.body.map(node => jsTreeifyMain(node, visited, true)) as es.Statement[]
×
2871
      let bodyIndex
2872
      const isEnd = pathIndex === endIndex
×
2873
      for (let i = 0; i < target.body.length; i++) {
×
2874
        if (path[pathIndex] === 'body[' + i + ']') {
×
2875
          bodyIndex = i
×
2876
          break
×
2877
        }
2878
      }
2879
      if (bodyIndex !== undefined) {
×
2880
        if (isEnd) {
×
2881
          redex = body[bodyIndex]
×
2882
          body[bodyIndex] = redexMarker as es.Statement
×
2883
        } else {
2884
          pathIndex++
×
2885
          body[bodyIndex] = pathify(target.body[bodyIndex]) as es.Statement
×
2886
        }
2887
      }
2888
      return ast.program(body)
×
2889
    },
2890

2891
    BlockStatement: (target: es.BlockStatement): es.BlockStatement => {
2892
      const body = target.body.map(node => jsTreeifyMain(node, visited, true)) as es.Statement[]
×
2893
      let bodyIndex
2894
      const isEnd = pathIndex === endIndex
×
2895
      for (let i = 0; i < target.body.length; i++) {
×
2896
        if (path[pathIndex] === 'body[' + i + ']') {
×
2897
          bodyIndex = i
×
2898
          break
×
2899
        }
2900
      }
2901
      if (bodyIndex !== undefined) {
×
2902
        if (isEnd) {
×
2903
          redex = body[bodyIndex]
×
2904
          body[bodyIndex] = redexMarker as es.Statement
×
2905
        } else {
2906
          pathIndex++
×
2907
          body[bodyIndex] = pathify(target.body[bodyIndex]) as es.Statement
×
2908
        }
2909
      }
2910
      return ast.blockStatement(body)
×
2911
    },
2912

2913
    BlockExpression: (target: BlockExpression): es.BlockStatement => {
2914
      const body = target.body.map(node => jsTreeifyMain(node, visited, true)) as es.Statement[]
×
2915
      let bodyIndex
2916
      const isEnd = pathIndex === endIndex
×
2917
      for (let i = 0; i < target.body.length; i++) {
×
2918
        if (path[pathIndex] === 'body[' + i + ']') {
×
2919
          bodyIndex = i
×
2920
          break
×
2921
        }
2922
      }
2923
      if (bodyIndex !== undefined) {
×
2924
        if (isEnd) {
×
2925
          redex = body[bodyIndex]
×
2926
          body[bodyIndex] = redexMarker as es.Statement
×
2927
        } else {
2928
          pathIndex++
×
2929
          body[bodyIndex] = pathify(target.body[bodyIndex]) as es.Statement
×
2930
        }
2931
      }
2932
      return ast.blockStatement(body)
×
2933
    },
2934

2935
    ReturnStatement: (target: es.ReturnStatement): es.ReturnStatement => {
2936
      let arg = jsTreeifyMain(target.argument!, visited, true) as es.Expression
×
2937
      if (path[pathIndex] === 'argument') {
×
2938
        if (pathIndex === endIndex) {
×
2939
          redex = arg
×
2940
          arg = redexMarker as es.Expression
×
2941
        } else {
2942
          pathIndex++
×
2943
          arg = pathify(target.argument!) as es.Expression
×
2944
        }
2945
      }
2946
      return ast.returnStatement(arg)
×
2947
    },
2948

2949
    // source 1
2950
    ArrowFunctionExpression: (
2951
      target: es.ArrowFunctionExpression
2952
    ): es.Identifier | es.ArrowFunctionExpression | es.FunctionDeclaration => {
2953
      let body = jsTreeifyMain(target.body, visited, true) as es.BlockStatement
×
2954
      if (path[pathIndex] === 'body') {
×
2955
        if (pathIndex === endIndex) {
×
2956
          redex = body
×
2957
          body = redexMarker as es.BlockStatement
×
2958
        } else {
2959
          pathIndex++
×
2960
          body = pathify(target.body) as es.BlockStatement
×
2961
        }
2962
      }
2963
      //localhost:8000
2964
      return ast.arrowFunctionExpression(target.params, body)
×
2965
    },
2966

2967
    VariableDeclaration: (target: es.VariableDeclaration): es.VariableDeclaration => {
2968
      const decl = target.declarations.map(node =>
×
2969
        jsTreeifyMain(node, visited, true)
×
2970
      ) as es.VariableDeclarator[]
2971
      let declIndex
2972
      const isEnd = pathIndex === endIndex
×
2973
      for (let i = 0; i < target.declarations.length; i++) {
×
2974
        if (path[pathIndex] === 'declarations[' + i + ']') {
×
2975
          declIndex = i
×
2976
          break
×
2977
        }
2978
      }
2979
      if (declIndex !== undefined) {
×
2980
        if (isEnd) {
×
2981
          redex = decl[declIndex]
×
2982
          decl[declIndex] = redexMarker as es.VariableDeclarator
×
2983
        } else {
2984
          pathIndex++
×
2985
          decl[declIndex] = pathify(target.declarations[declIndex]) as es.VariableDeclarator
×
2986
        }
2987
      }
2988
      return ast.variableDeclaration(decl)
×
2989
    },
2990

2991
    VariableDeclarator: (target: es.VariableDeclarator): es.VariableDeclarator => {
2992
      let init = jsTreeifyMain(target.init!, visited, true) as es.Expression
×
2993
      if (path[pathIndex] === 'init') {
×
2994
        if (pathIndex === endIndex) {
×
2995
          redex = init
×
2996
          init = redexMarker as es.Expression
×
2997
        } else {
2998
          pathIndex++
×
2999
          init = pathify(target.init!) as es.Expression
×
3000
        }
3001
      }
3002
      return ast.variableDeclarator(target.id, init)
×
3003
    },
3004

3005
    IfStatement: (target: es.IfStatement): es.IfStatement => {
3006
      let test = jsTreeifyMain(target.test, visited, true) as es.Expression
×
3007
      let cons = jsTreeifyMain(target.consequent, visited, true) as es.BlockStatement
×
3008
      let alt = jsTreeifyMain(target.alternate!, visited, true) as
×
3009
        | es.BlockStatement
3010
        | es.IfStatement
3011
      if (path[pathIndex] === 'test') {
×
3012
        if (pathIndex === endIndex) {
×
3013
          redex = test
×
3014
          test = redexMarker as es.Expression
×
3015
        } else {
3016
          pathIndex++
×
3017
          test = pathify(target.test) as es.Expression
×
3018
        }
3019
      } else if (path[pathIndex] === 'consequent') {
×
3020
        if (pathIndex === endIndex) {
×
3021
          redex = cons
×
3022
          cons = redexMarker as es.BlockStatement
×
3023
        } else {
3024
          pathIndex++
×
3025
          cons = pathify(target.consequent) as es.BlockStatement
×
3026
        }
3027
      } else if (path[pathIndex] === 'alternate') {
×
3028
        if (pathIndex === endIndex) {
×
3029
          redex = alt
×
3030
          alt = redexMarker as es.BlockStatement | es.IfStatement
×
3031
        } else {
3032
          pathIndex++
×
3033
          alt = pathify(target.alternate!) as es.BlockStatement | es.IfStatement
×
3034
        }
3035
      }
3036
      return ast.ifStatement(test, cons, alt)
×
3037
    },
3038

3039
    // source 2
3040
    ArrayExpression: (target: es.ArrayExpression): es.ArrayExpression => {
3041
      const eles = (target.elements as ContiguousArrayElements).map(node =>
×
3042
        jsTreeifyMain(node, visited, true)
×
3043
      ) as es.Expression[]
3044
      let eleIndex
3045
      const isEnd = pathIndex === endIndex
×
3046
      for (let i = 0; i < target.elements.length; i++) {
×
3047
        if (path[pathIndex] === 'elements[' + i + ']') {
×
3048
          eleIndex = i
×
3049
          break
×
3050
        }
3051
      }
3052
      if (eleIndex !== undefined) {
×
3053
        if (isEnd) {
×
3054
          redex = eles[eleIndex]
×
3055
          eles[eleIndex] = redexMarker as es.Expression
×
3056
        } else {
3057
          pathIndex++
×
3058
          eles[eleIndex] = pathify(
×
3059
            target.elements[eleIndex] as ContiguousArrayElementExpression
3060
          ) as es.Expression
3061
        }
3062
      }
3063
      return ast.arrayExpression(eles)
×
3064
    }
3065
  }
3066

3067
  function pathify(target: substituterNodes): substituterNodes {
3068
    const pathifier = pathifiers[target.type]
×
3069
    if (pathifier === undefined) {
×
3070
      return jsTreeifyMain(target, visited, true)
×
3071
    } else {
3072
      return pathifier(target)
×
3073
    }
3074
  }
3075

3076
  if (path === undefined || path[0] === undefined) {
×
3077
    return [jsTreeifyMain(target, visited, true), ast.program([])]
×
3078
  } else {
3079
    let pathified = pathify(target)
×
3080
    // runs pathify more than once if more than one substitution path
3081
    for (let i = 1; i < paths.length; i++) {
×
3082
      pathIndex = 0
×
3083
      path = paths[i]
×
3084
      endIndex = path === undefined ? 0 : path.length - 1
×
3085
      pathified = pathify(pathified)
×
3086
    }
3087
    return [pathified, redex]
×
3088
  }
3089
}
3090

3091
// Function to convert array from getEvaluationSteps into text
3092
export const redexify = (node: substituterNodes, path: string[][]): [string, string] => [
67✔
3093
  generate(pathifyMain(node, path, new Set())[0]),
3094
  generate(pathifyMain(node, path, new Set())[1])
3095
]
3096

3097
export const getRedex = (node: substituterNodes, path: string[][]): substituterNodes =>
67✔
3098
  pathifyMain(node, path, new Set())[1]
×
3099

3100
// strategy: we remember how many statements are there originally in program.
3101
// since listPrelude are just functions, they will be disposed of one by one
3102
// we prepend the program with the program resulting from the definitions,
3103
//   and reduce the combined program until the program body
3104
//   has number of statement === original program
3105
// then we return it to the getEvaluationSteps
3106
function substPredefinedFns(program: es.Program, context: Context): [es.Program, Context] {
3107
  if (context.prelude) {
56✔
3108
    // replace all occurences of '$' with 'helper_' to
3109
    // prevent collision with redex (temporary solution)
3110
    // context.prelude = context.prelude.replace(/\$/gi, 'helper_')
3111
    // evaluate the list prelude first
3112
    const listPreludeProgram = parse(context.prelude, context)!
7✔
3113
    const origBody = program.body as es.Statement[]
7✔
3114
    program.body = listPreludeProgram.body
7✔
3115
    program.body.push(ast.blockStatement(origBody))
7✔
3116
    while (program.body.length > 1) {
7✔
3117
      program = reduceMain(program, context)[0] as es.Program
213✔
3118
    }
3119
    program.body = (program.body[0] as es.BlockStatement).body
7✔
3120
  }
3121
  return [program, context]
56✔
3122
}
3123

3124
function substPredefinedConstants(program: es.Program): es.Program {
3125
  const constants = [['undefined', undefined]]
56✔
3126
  const mathConstants = Object.getOwnPropertyNames(Math)
56✔
3127
    .filter(name => typeof Math[name] !== 'function')
2,408✔
3128
    .map(name => ['math_' + name, Math[name]])
448✔
3129
  let substed = program
56✔
3130
  for (const nameValuePair of constants.concat(mathConstants)) {
56✔
3131
    substed = substituteMain(
504✔
3132
      ast.identifier(nameValuePair[0] as string),
3133
      ast.literal(nameValuePair[1] as string | number) as es.Literal,
3134
      substed,
3135
      [[]]
3136
    )[0] as es.Program
3137
  }
3138
  return substed
56✔
3139
}
3140

3141
function removeDebuggerStatements(program: es.Program): es.Program {
3142
  // recursively detect and remove debugger statements
3143
  function remove(removee: es.Program | es.Statement | es.Expression) {
3144
    if (removee.type === 'BlockStatement' || removee.type === 'Program') {
498✔
3145
      removee.body = removee.body.filter(s => s.type !== 'DebuggerStatement')
272✔
3146
      removee.body.forEach(s => remove(s as es.Statement))
270✔
3147
    } else if (removee.type === 'VariableDeclaration') {
369✔
3148
      removee.declarations.forEach(s => remove(s.init as es.Expression))
76✔
3149
    } else if (removee.type === 'FunctionDeclaration') {
293✔
3150
      remove(removee.body)
60✔
3151
    } else if (removee.type === 'IfStatement') {
233✔
3152
      remove(removee.consequent)
3✔
3153
      remove(removee.alternate as es.Statement)
3✔
3154
    } else if (removee.type === 'ArrowFunctionExpression') {
230✔
3155
      remove(removee.body)
30✔
3156
    }
3157
  }
3158
  remove(program)
56✔
3159
  return program
56✔
3160
}
3161

3162
async function evaluateImports(
3163
  program: es.Program,
3164
  context: Context,
3165
  { loadTabs, checkImports, wrapSourceModules }: ImportTransformOptions
3166
) {
3167
  const [importNodes, otherNodes] = partition(program.body, isImportDeclaration)
56✔
3168

3169
  const importNodeMap = importNodes.reduce((res, node) => {
56✔
3170
    const moduleName = node.source.value
×
3171
    assert(
×
3172
      typeof moduleName === 'string',
3173
      `ImportDeclarations should have string sources, got ${moduleName}`
3174
    )
3175

3176
    if (!(moduleName in res)) {
×
3177
      res[moduleName] = []
×
3178
    }
3179

3180
    res[moduleName].push(node)
×
3181
    return res
×
3182
  }, {} as Record<string, es.ImportDeclaration[]>)
3183

3184
  try {
56✔
3185
    await Promise.all(
56✔
3186
      Object.entries(importNodeMap).map(async ([moduleName, nodes]) => {
×
3187
        await initModuleContextAsync(moduleName, context, loadTabs)
×
3188
        const functions = await loadModuleBundleAsync(
×
3189
          moduleName,
3190
          context,
3191
          wrapSourceModules,
3192
          nodes[0]
3193
        )
3194
        const environment = currentEnvironment(context)
×
3195
        for (const node of nodes) {
×
3196
          for (const spec of node.specifiers) {
×
3197
            assert(
×
3198
              spec.type === 'ImportSpecifier',
3199
              `Only ImportSpecifiers are supported, got: ${spec.type}`
3200
            )
3201

3202
            if (checkImports && !(spec.imported.name in functions)) {
×
3203
              throw new UndefinedImportError(spec.imported.name, moduleName, spec)
×
3204
            }
3205
            declareIdentifier(context, spec.local.name, node, environment)
×
3206
            defineVariable(context, spec.local.name, functions[spec.imported.name], true, node)
×
3207
          }
3208
        }
3209
      })
3210
    )
3211
  } catch (error) {
3212
    // console.log(error)
3213
    handleRuntimeError(context, error)
×
3214
  }
3215
  program.body = otherNodes
56✔
3216
}
3217

3218
// the context here is for builtins
3219
export async function getEvaluationSteps(
67✔
3220
  program: es.Program,
3221
  context: Context,
3222
  { importOptions, stepLimit }: Pick<IOptions, 'importOptions' | 'stepLimit'>
3223
): Promise<[es.Program, string[][], string][]> {
3224
  const steps: [es.Program, string[][], string][] = []
56✔
3225
  try {
56✔
3226
    const limit = stepLimit === undefined ? 1000 : stepLimit % 2 === 0 ? stepLimit : stepLimit + 1
56!
3227
    await evaluateImports(program, context, importOptions)
56✔
3228
    // starts with substituting predefined constants
3229
    let start = substPredefinedConstants(program)
56✔
3230
    // and predefined fns
3231
    start = substPredefinedFns(start, context)[0]
56✔
3232
    // and remove debugger statements.
3233
    start = removeDebuggerStatements(start)
56✔
3234

3235
    // then add in path and explanation string
3236
    let reducedWithPath: [substituterNodes, Context, string[][], string] = [
56✔
3237
      start,
3238
      context,
3239
      [],
3240
      'Start of evaluation'
3241
    ]
3242
    // reduces program until evaluation completes
3243
    // even steps: program before reduction
3244
    // odd steps: program after reduction
3245
    let i = -1
56✔
3246
    let limitExceeded = false
56✔
3247
    while ((reducedWithPath[0] as es.Program).body.length > 0) {
56✔
3248
      if (steps.length === limit) {
1,744✔
3249
        steps[steps.length - 1] = [ast.program([]), [], 'Maximum number of steps exceeded']
1✔
3250
        limitExceeded = true
1✔
3251
        break
1✔
3252
      }
3253
      steps.push([
1,743✔
3254
        reducedWithPath[0] as es.Program,
3255
        reducedWithPath[2].length > 1 ? reducedWithPath[2].slice(1) : reducedWithPath[2],
1,743✔
3256
        reducedWithPath[3]
3257
      ])
3258
      steps.push([reducedWithPath[0] as es.Program, [], ''])
1,743✔
3259
      if (i > 0) {
1,743✔
3260
        steps[i][1] = reducedWithPath[2].length > 1 ? [reducedWithPath[2][0]] : reducedWithPath[2]
1,687✔
3261
        steps[i][2] = reducedWithPath[3]
1,687✔
3262
      }
3263
      reducedWithPath = reduceMain(reducedWithPath[0], context)
1,743✔
3264
      i += 2
1,736✔
3265
    }
3266
    if (!limitExceeded) {
49✔
3267
      steps[steps.length - 1][2] = 'Evaluation complete'
48✔
3268
    }
3269
    return steps
49✔
3270
  } catch (error) {
3271
    context.errors.push(error)
7✔
3272
    return steps
7✔
3273
  }
3274
}
3275

3276
export interface IStepperPropContents {
3277
  code: string
3278
  redex: string
3279
  explanation: string
3280
  function: es.Expression | undefined | es.Super
3281
}
3282

3283
export function isStepperOutput(output: any): output is IStepperPropContents {
67✔
3284
  return 'code' in output
×
3285
}
3286

3287
export function callee(
67✔
3288
  content: substituterNodes,
3289
  context: Context
3290
): es.Expression | undefined | es.Super {
3291
  if (content.type === 'CallExpression') {
×
3292
    let reducedArgs = true
×
3293
    for (const arg of content.arguments) {
×
3294
      if (!isIrreducible(arg, context)) {
×
3295
        reducedArgs = false
×
3296
      }
3297
    }
3298
    return reducedArgs ? content.callee : undefined
×
3299
  } else {
3300
    return undefined
×
3301
  }
3302
}
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