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

source-academy / js-slang / 24834367427

23 Apr 2026 12:09PM UTC coverage: 78.541% (+0.2%) from 78.391%
24834367427

Pull #1893

github

web-flow
Merge ab101147d into 715603479
Pull Request #1893: Error Handling and Stringify Changes

3126 of 4197 branches covered (74.48%)

Branch coverage included in aggregate %.

801 of 975 new or added lines in 76 files covered. (82.15%)

20 existing lines in 11 files now uncovered.

7056 of 8767 relevant lines covered (80.48%)

173930.4 hits per line

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

89.89
/src/utils/operators.ts
1
import type { BinaryOperator, UnaryOperator } from 'estree';
2

3
import {
4
  CallingNonFunctionValueError,
5
  ExceptionError,
6
  GetInheritedPropertyError,
7
  InvalidNumberOfArgumentsError,
8
} from '../errors/errors';
9
import { RuntimeSourceError } from '../errors/base';
10
import {
11
  PotentialInfiniteLoopError,
12
  PotentialInfiniteRecursionError,
13
} from '../errors/timeoutErrors';
14
import type { Chapter } from '../langs';
15
import type { NativeStorage } from '../types';
16
import * as create from './ast/astCreator';
17
import { callExpression, locationDummyNode } from './ast/astCreator';
18
import * as rttc from './rttc';
19

20
export function throwIfTimeout(
21
  nativeStorage: NativeStorage,
22
  start: number,
23
  current: number,
24
  line: number,
25
  column: number,
26
  source: string | null,
27
) {
28
  if (current - start > nativeStorage.maxExecTime) {
24,087,973✔
29
    throw new PotentialInfiniteLoopError(
2✔
30
      create.locationDummyNode(line, column, source),
31
      nativeStorage.maxExecTime,
32
    );
33
  }
34
}
35

36
export function boolOrErr(candidate: any, line: number, column: number, source: string | null) {
37
  rttc.checkIfStatement(create.locationDummyNode(line, column, source), candidate);
24,163,250✔
38
  return candidate;
24,163,250✔
39
}
40

41
export function unaryOp(
42
  operator: UnaryOperator,
43
  argument: any,
44
  line: number,
45
  column: number,
46
  source: string | null,
47
) {
48
  rttc.checkUnaryExpression(create.locationDummyNode(line, column, source), operator, argument);
10✔
49

50
  return evaluateUnaryExpression(operator, argument);
10✔
51
}
52

53
export function evaluateUnaryExpression(operator: UnaryOperator, value: any) {
54
  if (operator === '!') {
49✔
55
    return !value;
6✔
56
  } else if (operator === '-') {
43✔
57
    return -value;
42✔
58
  } else if (operator === 'typeof') {
1!
59
    return typeof value;
1✔
60
  } else {
61
    return +value;
×
62
  }
63
}
64

65
export function binaryOp(
66
  operator: BinaryOperator,
67
  chapter: Chapter,
68
  left: any,
69
  right: any,
70
  line: number,
71
  column: number,
72
  source: string | null,
73
) {
74
  rttc.checkBinaryExpression(create.locationDummyNode(line, column, source), operator, chapter, [
20,742,007✔
75
    left,
76
    right,
77
  ]);
78

79
  return evaluateBinaryExpression(operator, left, right);
20,742,007✔
80
}
81

82
export function evaluateBinaryExpression(operator: BinaryOperator, left: any, right: any) {
83
  switch (operator) {
21,590,833!
84
    case '+':
85
      return left + right;
20,683,608✔
86
    case '-':
87
      return left - right;
442,377✔
88
    case '*':
89
      return left * right;
45✔
90
    case '/':
91
      return left / right;
1✔
92
    case '%':
93
      return left % right;
×
94
    case '===':
95
      return left === right;
250,360✔
96
    case '!==':
97
      return left !== right;
×
98
    case '<=':
99
      return left <= right;
201,867✔
100
    case '<':
101
      return left < right;
2,305✔
102
    case '>':
103
      return left > right;
10,191✔
104
    case '>=':
105
      return left >= right;
79✔
106
    default:
107
      return undefined;
×
108
  }
109
}
110

111
interface FunctionDetails {
112
  /**
113
   * Name of the file/module that the function
114
   * was originally defined in
115
   */
116
  source: string | null;
117
  minArgsNeeded?: number;
118
}
119

120
const funcDetSymbol = Symbol();
72✔
121

122
function getFunctionDetails(f: Function): FunctionDetails {
123
  if (funcDetSymbol in f) {
11,788,746✔
124
    return f[funcDetSymbol] as FunctionDetails;
11,788,710✔
125
  }
126

127
  return {
36✔
128
    minArgsNeeded: f.length, // no way to check if the function hasVarArgs
129
    source: null,
130
  };
131
}
132

133
/**
134
 * Calls the provided value as if it were a function being called from the `line` and `column`
135
 * within the provided `source` file, checking for argument count.
136
 *
137
 * - If `nativeStorage` is provided, then infinite recursion protection is also added.
138
 */
139
export function callIfFuncAndRightArgs(
140
  f: unknown,
141
  line: number,
142
  column: number,
143
  source: string | null,
144
  nativeStorage: NativeStorage | undefined,
145
  ...args: any[]
146
) {
147
  const startTime = Date.now();
34,692✔
148
  const pastCalls: [string, any[]][] = [];
34,692✔
149
  let isPrelude = source === 'prelude';
34,692✔
150

151
  while (true) {
34,692✔
152
    const dummy = locationDummyNode(line, column, source);
11,637,837✔
153
    if (typeof f !== 'function') {
11,637,837!
NEW
154
      throw new CallingNonFunctionValueError(
×
155
        f,
156
        callExpression(dummy, args, {
157
          start: { line, column },
158
          end: { line, column },
159
          source,
160
        }),
161
      );
162
    }
163

164
    const expectedLength = f.length;
11,637,835✔
165
    const receivedLength = args.length;
11,637,835✔
166
    const { minArgsNeeded, source: funcSource } = getFunctionDetails(f);
11,637,835✔
167

168
    if (funcSource === 'prelude') {
11,637,835✔
169
      // Once we call into a prelude function, everything that follows
170
      // is in prelude code
171
      isPrelude = true;
9,917✔
172
    }
173

174
    const hasVarArgs = minArgsNeeded !== undefined;
11,637,835✔
175
    if (hasVarArgs ? minArgsNeeded > receivedLength : expectedLength !== receivedLength) {
11,637,835✔
176
      throw new InvalidNumberOfArgumentsError(
9✔
177
        callExpression(dummy, args, {
178
          start: { line, column },
179
          end: { line, column },
180
          source,
181
        }),
182
        hasVarArgs ? minArgsNeeded : expectedLength,
9✔
183
        receivedLength,
184
        f.name,
185
        hasVarArgs,
186
      );
187
    }
188

189
    let res;
190
    try {
11,637,826✔
191
      res = f(...args);
11,637,826✔
192

193
      if (nativeStorage && Date.now() - startTime > nativeStorage.maxExecTime) {
11,637,826✔
194
        throw new PotentialInfiniteRecursionError(dummy, pastCalls, nativeStorage.maxExecTime);
5✔
195
      }
196
    } catch (error) {
197
      // if we already handled the error, simply pass it on
198
      if (error instanceof ExceptionError) throw error;
14,104✔
199

200
      if (error instanceof RuntimeSourceError) {
93✔
201
        if (!error.node) {
73✔
202
          error.node = locationDummyNode(line, column, isPrelude ? 'prelude' : funcSource);
46✔
203
        } else if (funcSource) {
27✔
204
          if (!error.node.loc) {
19!
NEW
205
            error.node.loc = {
×
206
              start: { line, column },
207
              end: { line, column },
208
              source: isPrelude ? 'prelude' : funcSource,
×
209
            };
210
          } else {
211
            error.node.loc.source = isPrelude ? 'prelude' : funcSource;
19!
212
          }
213
        }
214
        throw error;
73✔
215
      }
216

217
      throw new ExceptionError(error);
20✔
218
    }
219

220
    // Limitations for current properTailCalls implementation:
221
    // Obviously, if objects ({}) are reintroduced,
222
    // we have to change this for a more stringent check,
223
    // as isTail and transformedFunctions are properties
224
    // and may be added by Source code.
225
    if (res === null || res === undefined) {
11,623,722✔
226
      return res;
162✔
227
    } else if (res.isTail === true) {
11,623,560✔
228
      f = res.function;
11,603,145✔
229
      args = res.arguments;
11,603,145✔
230
      source = res.source;
11,603,145✔
231
      line = res.line;
11,603,145✔
232
      column = res.column;
11,603,145✔
233
      pastCalls.push([res.functionName, args]);
11,603,145✔
234
      // Then go back to the top of the while loop
235
    } else if (res.isTail === false) {
20,415✔
236
      return res.value;
5,601✔
237
    } else {
238
      return res;
14,814✔
239
    }
240
  }
241
}
242

243
/**
244
 * Augment the given function with the necessary information for it to be called
245
 * properly by {@link callIfFuncAndRightArgs}. It won't redefine any existing details
246
 * that the function has already been wrapped with.
247
 *
248
 * - `hasVarArgs`
249
 *   - If `hasVarArgs` is `false` or `undefined`, then `f` is assumed not to have variadic args.
250
 *   - If `hasVarArgs` is `true`, `minArgsNeeded` is inferred from `f.length`.
251
 *   - If `hasVarArgs` is a number, it is used for `minArgsNeeded`.
252
 *
253
 * - If `stringified` is `undefined`, the function won't try to define `toReplString`.
254
 * - `funcName`
255
 *   - If `funcName` is `undefined`, the function won't try to define the `name` property.
256
 *   - If `funcName` is provided, the `name` property will get overriden.
257
 */
258
export function wrap<T extends (...args: any[]) => any>(
259
  f: T,
260
  hasVarArgs: boolean | undefined | number,
261
  stringified?: string,
262
  source: string | null = null,
186,479✔
263
  funcName?: string,
264
): T {
265
  let minArgsNeeded: number | undefined;
266
  if (hasVarArgs === true) {
186,479✔
267
    minArgsNeeded = f.length;
2✔
268
  } else if (typeof hasVarArgs === 'number') {
186,477✔
269
    minArgsNeeded = hasVarArgs;
23,804✔
270
  }
271

272
  if (funcName !== undefined) {
186,479✔
273
    Object.defineProperty(f, 'name', { value: funcName });
171,645✔
274
  }
275

276
  if (!(funcDetSymbol in f)) {
186,479✔
277
    (f as any)[funcDetSymbol] = {
35,568✔
278
      minArgsNeeded,
279
      source,
280
    };
281
  } else {
282
    const funcDets = getFunctionDetails(f);
150,911✔
283

284
    if (typeof funcDets.minArgsNeeded !== 'number') {
150,911✔
285
      funcDets.minArgsNeeded = minArgsNeeded;
136,548✔
286
    }
287

288
    if (typeof funcDets.source !== 'string') {
150,911!
289
      funcDets.source = source;
150,911✔
290
    }
291
  }
292

293
  if (stringified !== undefined && !('toReplString' in f)) {
186,479✔
294
    // Don't override toReplString if it was already defined
295
    // @ts-expect-error toReplString is not a known property of functions
296
    f.toReplString = () => stringified;
35,565✔
297
  }
298
  return f;
186,479✔
299
}
300

301
export function setProp(
302
  obj: any,
303
  prop: any,
304
  value: any,
305
  line: number,
306
  column: number,
307
  source: string | null,
308
) {
309
  const dummy = locationDummyNode(line, column, source);
126✔
310
  rttc.checkMemberAccess(dummy, [obj, prop]);
126✔
311
  return (obj[prop] = value);
126✔
312
}
313

314
export function getProp(obj: any, prop: any, line: number, column: number, source: string | null) {
315
  const dummy = locationDummyNode(line, column, source);
18✔
316
  rttc.checkMemberAccess(dummy, [obj, prop]);
18✔
317

318
  if (obj[prop] !== undefined && !obj.hasOwnProperty(prop)) {
18!
NEW
319
    throw new GetInheritedPropertyError(dummy, obj, prop);
×
320
  } else {
321
    return obj[prop];
18✔
322
  }
323
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc