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

source-academy / js-slang / 11929582114

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

Pull #1728

github

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

3654 of 4864 branches covered (75.12%)

Branch coverage included in aggregate %.

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

1 existing line in 1 file now uncovered.

11474 of 13663 relevant lines covered (83.98%)

143610.35 hits per line

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

91.65
/src/cse-machine/utils.ts
1
import * as es from 'estree'
2
import { isArray, isFunction } from 'lodash'
79✔
3

4
import { Context } from '..'
5
import * as errors from '../errors/errors'
79✔
6
import { RuntimeSourceError } from '../errors/runtimeSourceError'
7
import { Chapter, type Environment, type Node, type StatementSequence, type Value } from '../types'
79✔
8
import * as ast from '../utils/ast/astCreator'
79✔
9
import Heap from './heap'
79✔
10
import * as instr from './instrCreator'
79✔
11
import { Control, Transformers } from './interpreter'
12
import {
79✔
13
  AppInstr,
14
  EnvArray,
15
  ControlItem,
16
  Instr,
17
  InstrType,
18
  BranchInstr,
19
  WhileInstr,
20
  ForInstr
21
} from './types'
22
import Closure from './closure'
79✔
23
import { Continuation, isCallWithCurrentContinuation } from './continuations'
79✔
24
import { isApply, isEval } from './scheme-macros'
79✔
25
import { _Symbol } from '../alt-langs/scheme/scm-slang/src/stdlib/base'
79✔
26
import { is_number } from '../alt-langs/scheme/scm-slang/src/stdlib/core-math'
79✔
27

28
/**
29
 * Typeguard for commands to check if they are scheme values.
30
 *
31
 * @param command A ControlItem
32
 * @returns true if the ControlItem is a scheme value, false otherwise.
33
 */
34
export const isSchemeValue = (command: ControlItem): boolean => {
79✔
35
  return (
40,319,019✔
36
    command === null ||
241,890,950✔
37
    typeof command === 'string' ||
38
    typeof command === 'boolean' ||
39
    isArray(command) ||
40
    command instanceof _Symbol ||
41
    is_number(command)
42
  )
43
}
44

45
/**
46
 * Typeguard for Instr to distinguish between program statements and instructions.
47
 *
48
 * @param command A ControlItem
49
 * @returns true if the ControlItem is an instruction and false otherwise.
50
 */
51
export const isInstr = (command: ControlItem): command is Instr => {
79✔
52
  // this prevents us from reading properties of null
53
  if (isSchemeValue(command)) {
7,213,422✔
54
    return false
3,247✔
55
  }
56
  return (command as Instr).instrType !== undefined
7,210,175✔
57
}
58

59
/**
60
 * Typeguard for Node to distinguish between program statements and instructions.
61
 *
62
 * @param command A ControlItem
63
 * @returns true if the ControlItem is a Node or StatementSequence, false if it is an instruction.
64
 */
65
export const isNode = (command: ControlItem): command is Node => {
79✔
66
  // this prevents us from reading properties of null
67
  if (isSchemeValue(command)) {
33,105,597✔
68
    return false
9,035✔
69
  }
70
  return (command as Node).type !== undefined
33,096,562✔
71
}
72

73
/**
74
 * Typeguard for esIdentifier. To verify if a Node is an esIdentifier.
75
 *
76
 * @param node a Node
77
 * @returns true if node is an esIdentifier, false otherwise.
78
 */
79
export const isIdentifier = (node: Node): node is es.Identifier => {
79✔
80
  return (node as es.Identifier).name !== undefined
450,423✔
81
}
82

83
/**
84
 * Typeguard for esReturnStatement. To verify if a Node is an esReturnStatement.
85
 *
86
 * @param node a Node
87
 * @returns true if node is an esReturnStatement, false otherwise.
88
 */
89
export const isReturnStatement = (node: Node): node is es.ReturnStatement => {
79✔
90
  return (node as es.ReturnStatement).type == 'ReturnStatement'
30,098✔
91
}
92

93
/**
94
 * Typeguard for esIfStatement. To verify if a Node is an esIfStatement.
95
 *
96
 * @param node a Node
97
 * @returns true if node is an esIfStatement, false otherwise.
98
 */
99
export const isIfStatement = (node: Node): node is es.IfStatement => {
79✔
100
  return (node as es.IfStatement).type == 'IfStatement'
22,633✔
101
}
102

103
/**
104
 * Typeguard for esBlockStatement. To verify if a Node is a block statement.
105
 *
106
 * @param node a Node
107
 * @returns true if node is an esBlockStatement, false otherwise.
108
 */
109
export const isBlockStatement = (node: Node): node is es.BlockStatement => {
79✔
110
  return (node as es.BlockStatement).type == 'BlockStatement'
5,452,029✔
111
}
112

113
/**
114
 * Typeguard for StatementSequence. To verify if a ControlItem is a statement sequence.
115
 *
116
 * @param node a ControlItem
117
 * @returns true if node is a StatementSequence, false otherwise.
118
 */
119
export const isStatementSequence = (node: ControlItem): node is StatementSequence => {
79✔
120
  return (node as StatementSequence).type == 'StatementSequence'
14,421✔
121
}
122

123
/**
124
 * Typeguard for esRestElement. To verify if a Node is a block statement.
125
 *
126
 * @param node a Node
127
 * @returns true if node is an esRestElement, false otherwise.
128
 */
129
export const isRestElement = (node: Node): node is es.RestElement => {
79✔
130
  return (node as es.RestElement).type == 'RestElement'
610,580✔
131
}
132

133
/**
134
 * Generate a unique id, for use in environments, arrays and closures.
135
 *
136
 * @param context the context used to provide the new unique id
137
 * @returns a unique id
138
 */
139
export const uniqueId = (context: Context): string => {
79✔
140
  return `${context.runtime.objectCount++}`
806,094✔
141
}
142

143
/**
144
 * Returns whether `item` is an array with `id` and `environment` properties already attached.
145
 */
146
export const isEnvArray = (item: any): item is EnvArray => {
79✔
147
  return (
67,239✔
148
    isArray(item) &&
151,514✔
149
    {}.hasOwnProperty.call(item, 'id') &&
150
    {}.hasOwnProperty.call(item, 'environment')
151
  )
152
}
153

154
/**
155
 * Returns whether `item` is a non-closure function that returns a stream.
156
 * If the function has been called already and we have the result, we can
157
 * pass it in here as a 2nd argument for stronger checking
158
 */
159
export const isStreamFn = (item: any, result?: any): result is [any, () => any] => {
79✔
160
  if (result == null || !isArray(result) || result.length !== 2) return false
52,453✔
161
  return (
18,035✔
162
    isFunction(item) &&
72,139✔
163
    !(item instanceof Closure) &&
164
    (item.name === 'stream' || {}.hasOwnProperty.call(item, 'environment'))
165
  )
166
}
167

168
/**
169
 * Adds the properties `id` and `environment` to the given array, and adds the array to the
170
 * current environment's heap. Adds the array to the heap of `envOverride` instead if it's defined.
171
 *
172
 * @param context the context used to provide the current environment and new unique id
173
 * @param array the array to attach properties to, and for addition to the heap
174
 */
175
export const handleArrayCreation = (
79✔
176
  context: Context,
177
  array: any[],
178
  envOverride?: Environment
179
): void => {
180
  const environment = envOverride ?? currentEnvironment(context)
51,784✔
181
  // Both id and environment are non-enumerable so iterating
182
  // through the array will not return these values
183
  Object.defineProperties(array, {
51,784✔
184
    id: { value: uniqueId(context) },
185
    // Make environment writable as there are cases on the frontend where
186
    // environments of objects need to be modified
187
    environment: { value: environment, writable: true }
188
  })
189
  environment.heap.add(array as EnvArray)
51,784✔
190
}
191

192
/**
193
 * A helper function for handling sequences of statements.
194
 * Statements must be pushed in reverse order, and each statement is separated by a pop
195
 * instruction so that only the result of the last statement remains on stash.
196
 * Value producing statements have an extra pop instruction.
197
 *
198
 * @param seq Array of statements.
199
 * @returns Array of commands to be pushed into control.
200
 */
201
export const handleSequence = (seq: es.Statement[]): ControlItem[] => {
79✔
202
  const result: ControlItem[] = []
4,949✔
203
  let valueProduced = false
4,949✔
204
  for (const command of seq) {
4,949✔
205
    if (!isImportDeclaration(command)) {
28,166✔
206
      if (valueProducing(command)) {
28,160✔
207
        // Value producing statements have an extra pop instruction
208
        if (valueProduced) {
4,830✔
209
          result.push(instr.popInstr(command))
312✔
210
        } else {
211
          valueProduced = true
4,518✔
212
        }
213
      }
214
      result.push(command)
28,160✔
215
    }
216
  }
217
  // Push statements in reverse order
218
  return result.reverse()
4,949✔
219
}
220

221
/**
222
 * This function is used for ConditionalExpressions and IfStatements, to create the sequence
223
 * of control items to be added.
224
 */
225
export const reduceConditional = (
79✔
226
  node: es.IfStatement | es.ConditionalExpression
227
): ControlItem[] => {
228
  return [instr.branchInstr(node.consequent, node.alternate, node), node.test]
415,462✔
229
}
230

231
/**
232
 * To determine if a control item is value producing. JavaScript distinguishes value producing
233
 * statements and non-value producing statements.
234
 * Refer to https://sourceacademy.nus.edu.sg/sicpjs/4.1.2 exercise 4.8.
235
 *
236
 * @param command Control item to determine if it is value producing.
237
 * @returns true if it is value producing, false otherwise.
238
 */
239
export const valueProducing = (command: Node): boolean => {
79✔
240
  const type = command.type
591,406✔
241
  return (
591,406✔
242
    type !== 'VariableDeclaration' &&
3,564,650✔
243
    type !== 'FunctionDeclaration' &&
244
    type !== 'ContinueStatement' &&
245
    type !== 'BreakStatement' &&
246
    type !== 'DebuggerStatement' &&
247
    (type !== 'BlockStatement' || command.body.some(valueProducing))
248
  )
249
}
250

251
/**
252
 * To determine if a control item changes the environment.
253
 * There is a change in the environment when
254
 *  1. pushEnvironment() is called when creating a new frame, if there are variable declarations.
255
 *     Called in Program, BlockStatement, and Application instructions.
256
 *  2. there is an assignment.
257
 *     Called in Assignment and Array Assignment instructions.
258
 *  3. a new object is created.
259
 *     Called in ExpressionStatement that contains an ArrowFunctionExpression, or an ArrowFunctionExpression
260
 *
261
 * @param command Control item to check against.
262
 * @returns true if it changes the environment, false otherwise.
263
 */
264
export const envChanging = (command: ControlItem): boolean => {
79✔
265
  if (isNode(command)) {
7,159,601✔
266
    const type = command.type
5,215,409✔
267
    return (
5,215,409✔
268
      type === 'Program' ||
20,852,188✔
269
      type === 'BlockStatement' ||
270
      type === 'ArrowFunctionExpression' ||
271
      (type === 'ExpressionStatement' && command.expression.type === 'ArrowFunctionExpression')
272
    )
273
  } else if (isInstr(command)) {
1,944,192✔
274
    const type = command.instrType
1,943,883✔
275
    return (
1,943,883✔
276
      type === InstrType.ENVIRONMENT ||
9,824,219✔
277
      type === InstrType.ARRAY_LITERAL ||
278
      type === InstrType.ASSIGNMENT ||
279
      type === InstrType.ARRAY_ASSIGNMENT ||
280
      (type === InstrType.APPLICATION && (command as AppInstr).numOfArgs > 0)
281
    )
282
  } else {
283
    // TODO deal with scheme control items
284
    // for now, as per the CSE machine paper,
285
    // we decide to ignore environment optimizations
286
    // for scheme control items :P
287
    return true
309✔
288
  }
289
}
290

291
/**
292
 * To determine if the function is simple.
293
 * Simple functions contain a single return statement.
294
 *
295
 * @param node The function to check against.
296
 * @returns true if the function is simple, false otherwise.
297
 */
298
export const isSimpleFunction = (node: any) => {
79✔
299
  if (node.body.type !== 'BlockStatement' && node.body.type !== 'StatementSequence') {
550,525!
300
    return true
×
301
  } else {
302
    const block = node.body
550,525✔
303
    return block.body.length === 1 && block.body[0].type === 'ReturnStatement'
550,525✔
304
  }
305
}
306

307
/**
308
 * Transformers
309
 */
310
export const currentTransformers = (context: Context) =>
79✔
311
  context.runtime.transformers as Transformers
128,053✔
312

313
export const setTransformers = (context: Context, transformers: Transformers) => {
79✔
314
  context.runtime.transformers = transformers
657,430✔
315
}
316

317
/**
318
 * Environments
319
 */
320

321
export const currentEnvironment = (context: Context) => context.runtime.environments[0]
1,975,474✔
322

323
export const createEnvironment = (
79✔
324
  context: Context,
325
  closure: Closure,
326
  args: Value[],
327
  callExpression: es.CallExpression
328
): Environment => {
329
  const environment: Environment = {
450,423✔
330
    name: isIdentifier(callExpression.callee)
450,423✔
331
      ? callExpression.callee.name
332
      : closure.declaredName ?? closure.functionName,
12✔
333
    tail: closure.environment,
334
    head: {},
335
    heap: new Heap(),
336
    id: uniqueId(context),
337
    callExpression: {
338
      ...callExpression,
339
      arguments: args.map(ast.primitive)
340
    }
341
  }
342
  closure.node.params.forEach((param, index) => {
450,423✔
343
    if (isRestElement(param)) {
610,580✔
344
      const array = args.slice(index)
2✔
345
      handleArrayCreation(context, array, environment)
2✔
346
      environment.head[(param.argument as es.Identifier).name] = array
2✔
347
    } else {
348
      environment.head[(param as es.Identifier).name] = args[index]
610,578✔
349
    }
350
  })
351
  return environment
450,423✔
352
}
353

354
export const popEnvironment = (context: Context) => context.runtime.environments.shift()
108,717✔
355

356
export const pushEnvironment = (context: Context, environment: Environment) => {
79✔
357
  context.runtime.environments.unshift(environment)
455,184✔
358
  context.runtime.environmentTree.insert(environment)
455,184✔
359
}
360

361
export const createBlockEnvironment = (
79✔
362
  context: Context,
363
  name = 'blockEnvironment'
×
364
): Environment => {
365
  return {
4,762✔
366
    name,
367
    tail: currentEnvironment(context),
368
    head: {},
369
    heap: new Heap(),
370
    id: uniqueId(context)
371
  }
372
}
373

374
export const createProgramEnvironment = (context: Context, isPrelude: boolean): Environment => {
79✔
375
  return createBlockEnvironment(context, isPrelude ? 'prelude' : 'programEnvironment')
852✔
376
}
377

378
/**
379
 * Variables
380
 */
381

382
const UNASSIGNED_CONST = Symbol('const declaration')
79✔
383
const UNASSIGNED_LET = Symbol('let declaration')
79✔
384

385
export function declareIdentifier(
79✔
386
  context: Context,
387
  name: string,
388
  node: Node,
389
  environment: Environment,
390
  constant: boolean = false
5✔
391
) {
392
  if (environment.head.hasOwnProperty(name)) {
23,318!
393
    const descriptors = Object.getOwnPropertyDescriptors(environment.head)
×
394

395
    return handleRuntimeError(
×
396
      context,
397
      new errors.VariableRedeclaration(node, name, descriptors[name].writable)
398
    )
399
  }
400
  environment.head[name] = constant ? UNASSIGNED_CONST : UNASSIGNED_LET
23,318✔
401
  return environment
23,318✔
402
}
403

404
function declareVariables(
405
  context: Context,
406
  node: es.VariableDeclaration,
407
  environment: Environment
408
) {
409
  for (const declaration of node.declarations) {
4,442✔
410
    // Retrieve declaration type from node
411
    const constant = node.kind === 'const'
4,442✔
412
    declareIdentifier(context, (declaration.id as es.Identifier).name, node, environment, constant)
4,442✔
413
  }
414
}
415

416
export function declareFunctionsAndVariables(
79✔
417
  context: Context,
418
  node: es.BlockStatement,
419
  environment: Environment
420
) {
421
  for (const statement of node.body) {
4,761✔
422
    switch (statement.type) {
27,716✔
423
      case 'VariableDeclaration':
424
        declareVariables(context, statement, environment)
4,442✔
425
        break
4,442✔
426
      case 'FunctionDeclaration':
427
        // FunctionDeclaration is always of type constant
428
        declareIdentifier(
18,871✔
429
          context,
430
          (statement.id as es.Identifier).name,
431
          statement,
432
          environment,
433
          true
434
        )
435
        break
18,871✔
436
    }
437
  }
438
}
439

440
export function hasDeclarations(node: es.BlockStatement): boolean {
79✔
441
  for (const statement of node.body) {
266,989✔
442
    if (statement.type === 'VariableDeclaration' || statement.type === 'FunctionDeclaration') {
267,289✔
443
      return true
4,834✔
444
    }
445
  }
446
  return false
262,155✔
447
}
448

449
export function hasImportDeclarations(node: es.BlockStatement): boolean {
79✔
450
  for (const statement of (node as unknown as es.Program).body) {
183✔
451
    if (statement.type === 'ImportDeclaration') {
207!
452
      return true
×
453
    }
454
  }
455
  return false
183✔
456
}
457

458
function isImportDeclaration(node: Node): boolean {
459
  return node.type === 'ImportDeclaration'
28,166✔
460
}
461

462
export function defineVariable(
79✔
463
  context: Context,
464
  name: string,
465
  value: Value,
466
  constant = false,
×
467
  node: es.VariableDeclaration | es.ImportDeclaration
468
) {
469
  const environment = currentEnvironment(context)
24,079✔
470

471
  // we disable this check for full scheme due to the inability to scan for variables before usage
472
  if (
24,079!
473
    environment.head[name] !== UNASSIGNED_CONST &&
26,642✔
474
    environment.head[name] !== UNASSIGNED_LET &&
475
    context.chapter !== Chapter.FULL_SCHEME
476
  ) {
UNCOV
477
    return handleRuntimeError(context, new errors.VariableRedeclaration(node, name, !constant))
×
478
  }
479

480
  if (constant && value instanceof Closure) {
24,079✔
481
    value.declaredName = name
18,959✔
482
  }
483

484
  Object.defineProperty(environment.head, name, {
24,079✔
485
    value,
486
    writable: !constant,
487
    enumerable: true
488
  })
489

490
  return environment
24,079✔
491
}
492

493
export const getVariable = (context: Context, name: string, node: es.Identifier) => {
79✔
494
  let environment: Environment | null = currentEnvironment(context)
1,524,420✔
495
  while (environment) {
1,524,420✔
496
    if (environment.head.hasOwnProperty(name)) {
2,086,433✔
497
      if (
1,524,420✔
498
        environment.head[name] === UNASSIGNED_CONST ||
3,048,837✔
499
        environment.head[name] === UNASSIGNED_LET
500
      ) {
501
        return handleRuntimeError(context, new errors.UnassignedVariable(name, node))
3✔
502
      } else {
503
        return environment.head[name]
1,524,417✔
504
      }
505
    } else {
506
      environment = environment.tail
562,013✔
507
    }
508
  }
509
  return handleRuntimeError(context, new errors.UndefinedVariable(name, node))
×
510
}
511

512
export const setVariable = (
79✔
513
  context: Context,
514
  name: string,
515
  value: any,
516
  node: es.AssignmentExpression
517
) => {
518
  let environment: Environment | null = currentEnvironment(context)
3,484✔
519
  while (environment) {
3,484✔
520
    if (environment.head.hasOwnProperty(name)) {
8,259✔
521
      if (
3,484✔
522
        environment.head[name] === UNASSIGNED_CONST ||
6,968✔
523
        environment.head[name] === UNASSIGNED_LET
524
      ) {
525
        break
1✔
526
      }
527
      const descriptors = Object.getOwnPropertyDescriptors(environment.head)
3,483✔
528
      if (descriptors[name].writable) {
3,483✔
529
        environment.head[name] = value
3,479✔
530
        return undefined
3,479✔
531
      }
532
      return handleRuntimeError(context, new errors.ConstAssignment(node, name))
4✔
533
    } else {
534
      environment = environment.tail
4,775✔
535
    }
536
  }
537
  return handleRuntimeError(context, new errors.UndefinedVariable(name, node))
1✔
538
}
539

540
export const handleRuntimeError = (context: Context, error: RuntimeSourceError) => {
79✔
541
  context.errors.push(error)
95✔
542
  throw error
95✔
543
}
544

545
export const checkNumberOfArguments = (
79✔
546
  context: Context,
547
  callee: Closure | Value,
548
  args: Value[],
549
  exp: es.CallExpression
550
) => {
551
  if (callee instanceof Closure) {
604,176✔
552
    // User-defined or Pre-defined functions
553
    const params = callee.node.params
550,538✔
554
    const hasVarArgs = params[params.length - 1]?.type === 'RestElement'
550,538✔
555
    if (hasVarArgs ? params.length - 1 > args.length : params.length !== args.length) {
550,538✔
556
      return handleRuntimeError(
13✔
557
        context,
558
        new errors.InvalidNumberOfArguments(
559
          exp,
560
          hasVarArgs ? params.length - 1 : params.length,
13✔
561
          args.length,
562
          hasVarArgs
563
        )
564
      )
565
    }
566
  } else if (isCallWithCurrentContinuation(callee)) {
53,638✔
567
    // call/cc should have a single argument
568
    if (args.length !== 1) {
10✔
569
      return handleRuntimeError(
4✔
570
        context,
571
        new errors.InvalidNumberOfArguments(exp, 1, args.length, false)
572
      )
573
    }
574
    return undefined
6✔
575
  } else if (isEval(callee)) {
53,628✔
576
    // eval should have a single argument
577
    if (args.length !== 1) {
1,156!
NEW
578
      return handleRuntimeError(
×
579
        context,
580
        new errors.InvalidNumberOfArguments(exp, 1, args.length, false)
581
      )
582
    }
583
    return undefined
1,156✔
584
  } else if (isApply(callee)) {
52,472✔
585
    // apply should have at least two arguments
586
    if (args.length < 2) {
4✔
587
      return handleRuntimeError(
1✔
588
        context,
589
        new errors.InvalidNumberOfArguments(exp, 2, args.length, false)
590
      )
591
    }
592
    return undefined
3✔
593
  } else if (callee instanceof Continuation) {
52,468✔
594
    // Continuations have variadic arguments,
595
    // and so we can let it pass
596
    // TODO: in future, if we can somehow check the number of arguments
597
    // expected by the continuation, we can add a check here.
598
    return undefined
3✔
599
  } else {
600
    // Pre-built functions
601
    const hasVarArgs = callee.minArgsNeeded != undefined
52,465✔
602
    if (hasVarArgs ? callee.minArgsNeeded > args.length : callee.length !== args.length) {
52,465✔
603
      return handleRuntimeError(
4✔
604
        context,
605
        new errors.InvalidNumberOfArguments(
606
          exp,
607
          hasVarArgs ? callee.minArgsNeeded : callee.length,
4!
608
          args.length,
609
          hasVarArgs
610
        )
611
      )
612
    }
613
  }
614
  return undefined
602,986✔
615
}
616

617
/**
618
 * This function can be used to check for a stack overflow.
619
 * The current limit is set to be a control size of 1.0 x 10^5, if the control
620
 * flows beyond this limit an error is thrown.
621
 * This corresponds to about 10mb of space according to tests ran.
622
 */
623
export const checkStackOverFlow = (context: Context, control: Control) => {
79✔
624
  if (control.size() > 100000) {
604,202✔
625
    const stacks: es.CallExpression[] = []
4✔
626
    let counter = 0
4✔
627
    for (
4✔
628
      let i = 0;
4✔
629
      counter < errors.MaximumStackLimitExceeded.MAX_CALLS_TO_SHOW &&
28✔
630
      i < context.runtime.environments.length;
631
      i++
632
    ) {
633
      if (context.runtime.environments[i].callExpression) {
12✔
634
        stacks.unshift(context.runtime.environments[i].callExpression!)
12✔
635
        counter++
12✔
636
      }
637
    }
638
    handleRuntimeError(
4✔
639
      context,
640
      new errors.MaximumStackLimitExceeded(context.runtime.nodes[0], stacks)
641
    )
642
  }
643
}
644

645
/**
646
 * Checks whether an `if` statement returns in every possible branch.
647
 * @param body The `if` statement to be checked
648
 * @return `true` if every branch has a return statement, else `false`.
649
 */
650
export const hasReturnStatementIf = (statement: es.IfStatement): boolean => {
79✔
651
  let hasReturn = true
2,868✔
652
  // Parser enforces that if/else have braces (block statement)
653
  hasReturn = hasReturn && hasReturnStatement(statement.consequent as es.BlockStatement)
2,868✔
654
  if (statement.alternate) {
2,868✔
655
    if (isIfStatement(statement.alternate)) {
2,866!
656
      hasReturn = hasReturn && hasReturnStatementIf(statement.alternate as es.IfStatement)
×
657
    } else if (isBlockStatement(statement.alternate) || isStatementSequence(statement.alternate)) {
2,866!
658
      hasReturn = hasReturn && hasReturnStatement(statement.alternate)
2,866✔
659
    }
660
  }
661
  return hasReturn
2,868✔
662
}
663

664
/**
665
 * Checks whether a block returns in every possible branch.
666
 * @param body The block to be checked
667
 * @return `true` if every branch has a return statement, else `false`.
668
 */
669
export const hasReturnStatement = (block: es.BlockStatement | StatementSequence): boolean => {
79✔
670
  let hasReturn = false
24,255✔
671
  for (const statement of block.body) {
24,255✔
672
    if (isReturnStatement(statement)) {
30,098✔
673
      hasReturn = true
20,991✔
674
    } else if (isIfStatement(statement)) {
9,107✔
675
      // Parser enforces that if/else have braces (block statement)
676
      hasReturn = hasReturn || hasReturnStatementIf(statement as es.IfStatement)
2,870✔
677
    } else if (isBlockStatement(statement) || isStatementSequence(statement)) {
6,237✔
678
      hasReturn = hasReturn && hasReturnStatement(statement)
1!
679
    }
680
  }
681
  return hasReturn
24,255✔
682
}
683

684
export const hasBreakStatementIf = (statement: es.IfStatement): boolean => {
79✔
685
  let hasBreak = false
10✔
686
  // Parser enforces that if/else have braces (block statement)
687
  hasBreak = hasBreak || hasBreakStatement(statement.consequent as es.BlockStatement)
10✔
688
  if (statement.alternate) {
10✔
689
    if (isIfStatement(statement.alternate)) {
3!
690
      hasBreak = hasBreak || hasBreakStatementIf(statement.alternate)
3✔
691
    } else if (isBlockStatement(statement.alternate) || isStatementSequence(statement.alternate)) {
×
692
      hasBreak = hasBreak || hasBreakStatement(statement.alternate)
×
693
    }
694
  }
695
  return hasBreak
10✔
696
}
697

698
/**
699
 * Checks whether a block OR any of its child blocks has a `break` statement.
700
 * @param body The block to be checked
701
 * @return `true` if there is a `break` statement, else `false`.
702
 */
703
export const hasBreakStatement = (block: es.BlockStatement | StatementSequence): boolean => {
79✔
704
  let hasBreak = false
1,019✔
705
  for (const statement of block.body) {
1,019✔
706
    if (statement.type === 'BreakStatement') {
1,713✔
707
      hasBreak = true
5✔
708
    } else if (isIfStatement(statement)) {
1,708✔
709
      // Parser enforces that if/else have braces (block statement)
710
      hasBreak = hasBreak || hasBreakStatementIf(statement as es.IfStatement)
7✔
711
    } else if (isBlockStatement(statement) || isStatementSequence(statement)) {
1,701✔
712
      hasBreak = hasBreak || hasBreakStatement(statement)
664✔
713
    }
714
  }
715
  return hasBreak
1,019✔
716
}
717

718
export const hasContinueStatementIf = (statement: es.IfStatement): boolean => {
79✔
719
  let hasContinue = false
28✔
720
  // Parser enforces that if/else have braces (block statement)
721
  hasContinue = hasContinue || hasContinueStatement(statement.consequent as es.BlockStatement)
28✔
722
  if (statement.alternate) {
28✔
723
    if (isIfStatement(statement.alternate)) {
6!
724
      hasContinue = hasContinue || hasContinueStatementIf(statement.alternate)
6✔
725
    } else if (isBlockStatement(statement.alternate) || isStatementSequence(statement.alternate)) {
×
726
      hasContinue = hasContinue || hasContinueStatement(statement.alternate)
×
727
    }
728
  }
729
  return hasContinue
28✔
730
}
731

732
/**
733
 * Checks whether a block OR any of its child blocks has a `continue` statement.
734
 * @param body The block to be checked
735
 * @return `true` if there is a `continue` statement, else `false`.
736
 */
737
export const hasContinueStatement = (block: es.BlockStatement | StatementSequence): boolean => {
79✔
738
  let hasContinue = false
5,336✔
739
  for (const statement of block.body) {
5,336✔
740
    if (statement.type === 'ContinueStatement') {
8,961✔
741
      hasContinue = true
18✔
742
    } else if (isIfStatement(statement)) {
8,943✔
743
      // Parser enforces that if/else have braces (block statement)
744
      hasContinue = hasContinue || hasContinueStatementIf(statement as es.IfStatement)
25✔
745
    } else if (isBlockStatement(statement) || isStatementSequence(statement)) {
8,918✔
746
      hasContinue = hasContinue || hasContinueStatement(statement)
3,510✔
747
    }
748
  }
749
  return hasContinue
5,336✔
750
}
751

752
type PropertySetter = Map<string, Transformer>
753
type Transformer = (item: ControlItem) => ControlItem
754

755
const setToTrue = (item: ControlItem): ControlItem => {
79✔
756
  item.isEnvDependent = true
749,932✔
757
  return item
749,932✔
758
}
759

760
const setToFalse = (item: ControlItem): ControlItem => {
79✔
761
  item.isEnvDependent = false
1,562,141✔
762
  return item
1,562,141✔
763
}
764

765
const propertySetter: PropertySetter = new Map<string, Transformer>([
79✔
766
  [
767
    'Program',
768
    (node: Node) => {
769
      node = node as es.Program
1,034✔
770
      node.isEnvDependent = node.body.some(elem => isEnvDependent(elem))
1,055✔
771
      return node
1,034✔
772
    }
773
  ],
774
  ['Literal', setToFalse],
775
  ['ImportDeclaration', setToFalse],
776
  ['BreakStatement', setToFalse],
777
  ['ContinueStatement', setToFalse],
778
  ['DebuggerStatement', setToFalse],
779
  ['VariableDeclaration', setToTrue],
780
  ['FunctionDeclaration', setToTrue],
781
  ['ArrowFunctionExpression', setToTrue],
782
  ['Identifier', setToTrue],
783
  [
784
    'LogicalExpression',
785
    (node: Node) => {
786
      node = node as es.LogicalExpression
5✔
787
      node.isEnvDependent = isEnvDependent(node.left) || isEnvDependent(node.right)
5✔
788
      return node
5✔
789
    }
790
  ],
791
  [
792
    'BinaryExpression',
793
    (node: Node) => {
794
      node = node as es.BinaryExpression
764✔
795
      node.isEnvDependent = isEnvDependent(node.left) || isEnvDependent(node.right)
764✔
796
      return node
764✔
797
    }
798
  ],
799
  [
800
    'UnaryExpression',
801
    (node: Node) => {
802
      node = node as es.UnaryExpression
4✔
803
      node.isEnvDependent = isEnvDependent(node.argument)
4✔
804
      return node
4✔
805
    }
806
  ],
807
  [
808
    'ConditionalExpression',
809
    (node: Node) => {
810
      node = node as es.ConditionalExpression
10,041✔
811
      node.isEnvDependent =
10,041✔
812
        isEnvDependent(node.consequent) ||
20,051!
813
        isEnvDependent(node.alternate) ||
814
        isEnvDependent(node.test)
815
      return node
10,041✔
816
    }
817
  ],
818
  [
819
    'MemberExpression',
820
    (node: Node) => {
821
      node = node as es.MemberExpression
8✔
822
      node.isEnvDependent = isEnvDependent(node.property) || isEnvDependent(node.object)
8✔
823
      return node
8✔
824
    }
825
  ],
826
  [
827
    'ArrayExpression',
828
    (node: Node) => {
829
      node = node as es.ArrayExpression
321✔
830
      node.isEnvDependent = node.elements.some(elem => isEnvDependent(elem))
330✔
831
      return node
321✔
832
    }
833
  ],
834
  [
835
    'AssignmentExpression',
836
    (node: Node) => {
837
      node = node as es.AssignmentExpression
1,024✔
838
      node.isEnvDependent = isEnvDependent(node.left) || isEnvDependent(node.right)
1,024!
839
      return node
1,024✔
840
    }
841
  ],
842
  [
843
    'ReturnStatement',
844
    (node: Node) => {
845
      node = node as es.ReturnStatement
80✔
846
      node.isEnvDependent = isEnvDependent(node.argument)
80✔
847
      return node
80✔
848
    }
849
  ],
850
  [
851
    'CallExpression',
852
    (node: Node) => {
853
      node = node as es.CallExpression
53,370✔
854
      node.isEnvDependent =
53,370✔
855
        isEnvDependent(node.callee) || node.arguments.some(arg => isEnvDependent(arg))
7✔
856
      return node
53,370✔
857
    }
858
  ],
859
  [
860
    'ExpressionStatement',
861
    (node: Node) => {
862
      node = node as es.ExpressionStatement
707✔
863
      node.isEnvDependent = isEnvDependent(node.expression)
707✔
864
      return node
707✔
865
    }
866
  ],
867
  [
868
    'IfStatement',
869
    (node: Node) => {
870
      node = node as es.IfStatement
36✔
871
      node.isEnvDependent =
36✔
872
        isEnvDependent(node.test) ||
46✔
873
        isEnvDependent(node.consequent) ||
874
        isEnvDependent(node.alternate)
875
      return node
36✔
876
    }
877
  ],
878
  [
879
    'ForStatement',
880
    (node: Node) => {
881
      node = node as es.ForStatement
678✔
882
      node.isEnvDependent =
678✔
883
        isEnvDependent(node.body) ||
679!
884
        isEnvDependent(node.init) ||
885
        isEnvDependent(node.test) ||
886
        isEnvDependent(node.update)
887
      return node
678✔
888
    }
889
  ],
890
  [
891
    'WhileStatement',
892
    (node: Node) => {
893
      node = node as es.WhileStatement
8✔
894
      node.isEnvDependent = isEnvDependent(node.body) || isEnvDependent(node.test)
8!
895
      return node
8✔
896
    }
897
  ],
898
  [
899
    'BlockStatement',
900
    (node: Node) => {
901
      node = node as es.BlockStatement
1,407✔
902
      node.isEnvDependent = node.body.some(stm => isEnvDependent(stm))
1,408✔
903
      return node
1,407✔
904
    }
905
  ],
906
  [
907
    'StatementSequence',
908
    (node: Node) => {
909
      node = node as StatementSequence
266,750✔
910
      node.isEnvDependent = node.body.some(stm => isEnvDependent(stm))
266,775✔
911
      return node
266,750✔
912
    }
913
  ],
914

915
  [
916
    'ImportDeclaration',
917
    (node: Node) => {
918
      node = node as es.ImportDeclaration
6✔
919
      node.isEnvDependent = node.specifiers.some(x => isEnvDependent(x))
6✔
920
      return node
6✔
921
    }
922
  ],
923

924
  ['ImportSpecifier', setToTrue],
925

926
  ['ImportDefaultSpecifier', setToTrue],
927

928
  //Instruction
929
  [InstrType.RESET, setToFalse],
930
  [InstrType.UNARY_OP, setToFalse],
931
  [InstrType.BINARY_OP, setToFalse],
932
  [InstrType.POP, setToFalse],
933
  [InstrType.ARRAY_ACCESS, setToFalse],
934
  [InstrType.ARRAY_ASSIGNMENT, setToFalse],
935
  [InstrType.CONTINUE, setToFalse],
936
  [InstrType.CONTINUE_MARKER, setToFalse],
937
  [InstrType.BREAK_MARKER, setToFalse],
938
  [InstrType.MARKER, setToFalse],
939
  [InstrType.ENVIRONMENT, setToFalse],
940
  [InstrType.APPLICATION, setToTrue],
941
  [InstrType.ASSIGNMENT, setToTrue],
942
  [InstrType.ARRAY_LITERAL, setToTrue],
943
  [
944
    InstrType.WHILE,
945
    (instr: WhileInstr) => {
946
      instr.isEnvDependent = isEnvDependent(instr.test) || isEnvDependent(instr.body)
14✔
947
      return instr
14✔
948
    }
949
  ],
950
  [
951
    InstrType.FOR,
952
    (instr: ForInstr) => {
953
      instr.isEnvDependent =
331✔
954
        isEnvDependent(instr.init) ||
331!
955
        isEnvDependent(instr.test) ||
956
        isEnvDependent(instr.update) ||
957
        isEnvDependent(instr.body)
958
      return instr
331✔
959
    }
960
  ],
961
  [
962
    InstrType.BRANCH,
963
    (instr: BranchInstr) => {
964
      instr.isEnvDependent = isEnvDependent(instr.consequent) || isEnvDependent(instr.alternate)
415,462✔
965
      return instr
415,462✔
966
    }
967
  ]
968
])
969

970
/**
971
 * Checks whether the evaluation of the given control item depends on the current environment.
972
 * The item is also considered environment dependent if its evaluation introduces
973
 * environment dependent items
974
 * @param item The control item to be checked
975
 * @return `true` if the item is environment depedent, else `false`.
976
 */
977

978
export function isEnvDependent(item: ControlItem | null | undefined): boolean {
79✔
979
  if (item === null || item === undefined) {
16,684,228✔
980
    return false
16✔
981
  }
982

983
  // Scheme primitives are not environment dependent.
984
  if (typeof item === 'string' || typeof item === 'boolean') {
16,684,212✔
985
    return false
18✔
986
  }
987

988
  // Scheme symbols represent identifiers, which are environment dependent.
989
  if (item instanceof _Symbol) {
16,684,194✔
990
    return true
452✔
991
  }
992

993
  // We assume no optimisations for scheme lists.
994
  if (isArray(item)) {
16,683,742✔
995
    return true
5,270✔
996
  }
997

998
  // If result is already calculated, return it
999
  if (item.isEnvDependent !== undefined) {
16,678,472✔
1000
    return item.isEnvDependent
13,614,251✔
1001
  }
1002

1003
  const setter = isNode(item)
3,064,221✔
1004
    ? propertySetter.get(item.type)
1005
    : isInstr(item)
2,565,646✔
1006
    ? propertySetter.get(item.instrType)
1007
    : undefined
1008

1009
  if (setter) {
3,064,221✔
1010
    return setter(item)?.isEnvDependent ?? false
3,064,123!
1011
  }
1012

1013
  return false
98✔
1014
}
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