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

source-academy / js-slang / 24868044425

24 Apr 2026 01:47AM UTC coverage: 78.522% (+0.1%) from 78.391%
24868044425

push

github

web-flow
Error Handling and Stringify Changes (#1893)

* Modify stringify to prioritize toReplString

* Make the extract declarations helper actually work

* Add ability to change loader for source modules

* Add a new option for controlling how Source modules are loaded

* Improve typing for CSE machine

* Add ability to check if modules are loaded with the wrong Source chapter

* Refactor errors to extend from Error class

* Refactor modules errors

* Refactor parser errors

* Refactor cse machine errors

* Mostly fix error handling in the tracer

* Tidy up generator and explainer implementations for tracer

* Remove unnecessary imports and type guards

* Adjust rttc checks to be type guards instead of returning errors

* Adjust miscellanous error changes

* Add eslint rule for useless constructor

* Fix incorrect ordering for checking exceptionerrors

* Minor changes

* Run format

* Run linting

* Override the message property, but also enable noImplicitOverride to prevent accidental overriding

* Add some documentation to some errors

* Add errors to possible imports for modules

* Update getIds helper

* Minor fix to test case

* Run format

* Allow modules to try and load the context of other modules without crashing

* Fix incorrect stdlib name

* Add some more functions to the stdlib list library

* Change to use GeneralRuntimeError for list

* Revert "Change to use GeneralRuntimeError for list"

This reverts commit 642bd99e6.

* Update src/errors/errors.ts

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

* Add ability to change manifest and docs importers

* Change how external builtins are defined

* Fix typings and list lib overloads

* Miscellanous changes

* Add the Source equality function to stdlib/misc

* Merge from main branch for tracer

* Add handling for the new importers

* Change errors and made redex a local variable

* Improve tracer typing

* Relocate... (continued)

3125 of 4193 branches covered (74.53%)

Branch coverage included in aggregate %.

899 of 1089 new or added lines in 96 files covered. (82.55%)

21 existing lines in 12 files now uncovered.

7031 of 8741 relevant lines covered (80.44%)

185057.72 hits per line

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

74.79
/src/stdlib/parser.ts
1
import type es from 'estree';
2

3
import { parse as sourceParse } from '../parser/parser';
4
import { SourceParser } from '../parser/source';
5
import { libraryParserLanguage } from '../parser/source/syntax';
6
import type { Context, ContiguousArrayElements, Node, NodeTypeToNode, Value } from '../types';
7
import { getSourceVariableDeclaration } from '../utils/ast/helpers';
8
import { isDeclaration } from '../utils/ast/typeGuards';
9
import { oneLine } from '../utils/formatters';
10
import { RuntimeSourceError } from '../errors/base';
11
import { vector_to_list, type List } from './list';
12

13
class ParseError extends RuntimeSourceError<Node | undefined> {
14
  private readonly explanation: string;
15

16
  constructor(explanation: string, parseFuncName: string, node?: Node) {
17
    super(node);
4✔
18
    this.explanation = `${parseFuncName}: ${explanation}`;
4✔
19
  }
20

21
  public override explain(): string {
22
    return this.explanation;
4✔
23
  }
24
}
25

26
function unreachable() {
27
  console.error(oneLine`
×
28
    UNREACHABLE CODE REACHED!
29
    Please file an issue at
30
    https://github.com/source-academy/js-slang/issues
31
    if you see this.
32
  `);
33
}
34

35
interface TransformerData {
36
  transform(node: Node): List;
37
  makeSequenceIfNeeded(exs: Node[]): List;
38
  makeBlockIfNeeded(exs: Node[]): List;
39
  funcName: string;
40
}
41

42
// sequences of expressions of length 1
43
// can be represented by the element itself,
44
// instead of constructing a sequence
45

46
// checks if sequence has declaration at toplevel
47
// (outside of any block)
48
function hasDeclarationAtToplevel(exs: Node[]) {
49
  return exs.some(isDeclaration);
30✔
50
}
51

52
type ParseTransformer<T extends Node> = (this: TransformerData, node: T) => List;
53
type ASTTransformers = {
54
  [K in Node['type']]?: ParseTransformer<NodeTypeToNode<K>>;
55
};
56

57
const transformers: ASTTransformers = {
63✔
58
  ArrayExpression({ elements }) {
59
    return vector_to_list([
6✔
60
      'array_expression',
61
      vector_to_list((elements as ContiguousArrayElements).map(this.transform)),
62
    ]);
63
  },
64
  ArrowFunctionExpression({ body, params }) {
65
    return vector_to_list([
13✔
66
      'lambda_expression',
67
      vector_to_list(params.map(this.transform)),
68
      body.type === 'BlockStatement'
13✔
69
        ? // body.body: strip away one layer of block:
70
          // The body of a function is the statement
71
          // inside the curly braces.
72
          this.makeBlockIfNeeded(body.body)
73
        : vector_to_list(['return_statement', this.transform(body)]),
74
    ]);
75
  },
76
  AssignmentExpression(node) {
77
    if (node.left.type === 'Identifier') {
24✔
78
      return vector_to_list(['assignment', this.transform(node.left), this.transform(node.right)]);
16✔
79
    } else if (node.left.type === 'MemberExpression') {
8!
80
      return vector_to_list([
8✔
81
        'object_assignment',
82
        this.transform(node.left),
83
        this.transform(node.right),
84
      ]);
85
    } else {
86
      unreachable();
×
NEW
87
      throw new ParseError('Invalid assignment', this.funcName, node);
×
88
    }
89
  },
90
  BinaryExpression(node) {
91
    return vector_to_list([
44✔
92
      'binary_operator_combination',
93
      node.operator,
94
      this.transform(node.left),
95
      this.transform(node.right),
96
    ]);
97
  },
98
  BlockStatement({ body }) {
99
    return this.makeBlockIfNeeded(body);
20✔
100
  },
101
  BreakStatement: () => vector_to_list(['break_statement']),
4✔
102
  CallExpression({ callee, arguments: args }) {
103
    return vector_to_list([
18✔
104
      'application',
105
      this.transform(callee),
106
      vector_to_list(args.map(this.transform)),
107
    ]);
108
  },
109
  ClassDeclaration(node) {
UNCOV
110
    return vector_to_list([
×
111
      'class_declaration',
112
      vector_to_list([
113
        'name',
114
        node.id?.name,
115
        !node.superClass ? null : this.transform(node.superClass),
×
116
        node.body.body.map(this.transform),
117
      ]),
118
    ]);
119
  },
120
  ConditionalExpression(node) {
121
    return vector_to_list([
3✔
122
      'conditional_expression',
123
      this.transform(node.test),
124
      this.transform(node.consequent),
125
      this.transform(node.alternate),
126
    ]);
127
  },
128
  ContinueStatement: () => vector_to_list(['continue_statement']),
4✔
129
  ExportDefaultDeclaration(node) {
130
    return vector_to_list(['export_default_declaration', this.transform(node.declaration)]);
4✔
131
  },
132
  ExportNamedDeclaration({ declaration, specifiers }) {
133
    return vector_to_list([
8✔
134
      'export_named_declaration',
135
      declaration ? this.transform(declaration) : specifiers.map(this.transform),
8✔
136
    ]);
137
  },
138
  ExportSpecifier: node => vector_to_list(['name', node.exported.name]),
2✔
139
  ExpressionStatement({ expression }) {
140
    return this.transform(expression);
75✔
141
  },
142
  ForStatement(node) {
143
    return vector_to_list([
4✔
144
      'for_loop',
145
      this.transform(node.init!),
146
      this.transform(node.test!),
147
      this.transform(node.update!),
148
      this.transform(node.body),
149
    ]);
150
  },
151
  FunctionDeclaration({ id, params, body }) {
152
    return vector_to_list([
8✔
153
      'function_declaration',
154
      this.transform(id!),
155
      vector_to_list(params.map(this.transform)),
156
      this.makeBlockIfNeeded(body.body),
157
    ]);
158
  },
159
  FunctionExpression({ body: { body }, params }) {
NEW
160
    return vector_to_list([
×
161
      'lambda_expression',
162
      vector_to_list(params.map(this.transform)),
163
      this.makeBlockIfNeeded(body),
164
    ]);
165
  },
166
  Identifier: ({ name }) => vector_to_list(['name', name]),
179✔
167
  IfStatement(node) {
168
    return vector_to_list([
8✔
169
      'conditional_statement',
170
      this.transform(node.test),
171
      this.transform(node.consequent),
172
      node.alternate == null ? this.makeSequenceIfNeeded([]) : this.transform(node.alternate),
8✔
173
    ]);
174
  },
175
  ImportDeclaration(node) {
176
    return vector_to_list([
3✔
177
      'import_declaration',
178
      vector_to_list(node.specifiers.map(this.transform)),
179
      node.source.value,
180
    ]);
181
  },
182
  ImportDefaultSpecifier: () => vector_to_list(['default']),
2✔
183
  ImportSpecifier: node => vector_to_list(['name', node.imported.name]),
1✔
184
  Literal: ({ value }) => vector_to_list(['literal', value]),
159✔
185
  LogicalExpression(node) {
186
    return vector_to_list([
7✔
187
      'logical_composition',
188
      node.operator,
189
      this.transform(node.left),
190
      this.transform(node.right),
191
    ]);
192
  },
193
  MemberExpression(node) {
194
    // "computed" property of MemberExpression distinguishes
195
    // between dot access (not computed) and
196
    // a[...] (computed)
197
    // the key in dot access is meant as string, and
198
    // represented by a "property" node in parse result
199
    return vector_to_list([
19✔
200
      'object_access',
201
      this.transform(node.object),
202
      !node.computed && node.property.type === 'Identifier'
43✔
203
        ? vector_to_list(['property', node.property.name])
204
        : this.transform(node.property),
205
    ]);
206
  },
207
  MethodDefinition(node) {
NEW
208
    return vector_to_list([
×
209
      'method_definition',
210
      node.kind,
211
      node.static,
212
      this.transform(node.key),
213
      this.transform(node.value),
214
    ]);
215
  },
216
  NewExpression({ callee, arguments: args }) {
NEW
217
    return vector_to_list([
×
218
      'new_expression',
219
      this.transform(callee),
220
      vector_to_list(args.map(this.transform)),
221
    ]);
222
  },
223
  ObjectExpression({ properties }) {
224
    return vector_to_list(['object_expression', vector_to_list(properties.map(this.transform))]);
11✔
225
  },
226
  Program({ body }) {
227
    return this.makeSequenceIfNeeded(body);
68✔
228
  },
229
  Property(node) {
230
    // identifiers before the ":" in literal objects are meant
231
    // as string, and represented by a "property" node in parse result
232
    return vector_to_list([
21✔
233
      'key_value_pair',
234
      node.key.type === 'Identifier'
21✔
235
        ? vector_to_list(['property', node.key.name])
236
        : this.transform(node.key),
237
      this.transform(node.value),
238
    ]);
239
  },
240
  RestElement({ argument }) {
241
    return vector_to_list(['rest_element', this.transform(argument)]);
1✔
242
  },
243
  ReturnStatement(node) {
244
    return vector_to_list(['return_statement', this.transform(node.argument!)]);
13✔
245
  },
246
  SpreadElement({ argument }) {
247
    return vector_to_list(['spread_element', this.transform(argument)]);
1✔
248
  },
249
  StatementSequence({ body }) {
NEW
250
    return this.makeSequenceIfNeeded(body);
×
251
  },
252
  Super: () => vector_to_list(['super_expression']),
×
253
  ThisExpression: () => vector_to_list(['this_expression']),
×
254
  ThrowStatement({ argument }) {
NEW
255
    return vector_to_list(['throw_statement', this.transform(argument)]);
×
256
  },
257
  TryStatement(node) {
UNCOV
258
    return vector_to_list([
×
259
      'try_statement',
260
      this.transform(node.block),
261
      !node.handler ? null : vector_to_list(['name', (node.handler.param as es.Identifier).name]),
×
262
      !node.handler ? null : this.transform(node.handler.body),
×
263
    ]);
264
  },
265
  UnaryExpression({ operator, argument }) {
266
    return vector_to_list([
5✔
267
      'unary_operator_combination',
268
      operator === '-' ? '-unary' : operator,
5✔
269
      this.transform(argument),
270
    ]);
271
  },
272
  VariableDeclaration(node) {
273
    const { id, init } = getSourceVariableDeclaration(node);
28✔
274

275
    if (node.kind === 'let') {
28✔
276
      return vector_to_list(['variable_declaration', this.transform(id), this.transform(init)]);
15✔
277
    } else if (node.kind === 'const') {
13!
278
      return vector_to_list(['constant_declaration', this.transform(id), this.transform(init)]);
13✔
279
    } else {
280
      unreachable();
×
NEW
281
      throw new ParseError(
×
282
        `Invalid declaration kind for VariableDeclaration: ${node.kind}`,
283
        this.funcName,
284
        node,
285
      );
286
    }
287
  },
288
  WhileStatement({ test, body }) {
289
    return vector_to_list(['while_loop', this.transform(test), this.transform(body)]);
2✔
290
  },
291
};
292

293
export function parse(x: string, context: Context): Value {
294
  context.chapter = libraryParserLanguage;
71✔
295
  const program = sourceParse(x, context);
71✔
296
  if (context.errors.length > 0) {
71✔
297
    throw new ParseError(context.errors[0].explain(), parse.name);
3✔
298
  }
299

300
  function transform(node: Node) {
301
    if (!(node.type in transformers)) {
765!
NEW
302
      unreachable();
×
NEW
303
      throw new ParseError(`Cannot transform unknown node type: ${node.type}`, parse.name, node);
×
304
    }
305

306
    const transformer = transformers[node.type] as ParseTransformer<Node>;
765✔
307
    return transformer.call(
765✔
308
      {
309
        funcName: parse.name,
310
        transform,
311
        makeBlockIfNeeded,
312
        makeSequenceIfNeeded,
313
      },
314
      node,
315
    );
316
  }
317

318
  function makeSequenceIfNeeded(exs: Node[]): List {
319
    return exs.length === 1
99✔
320
      ? transform(exs[0])
321
      : vector_to_list(['sequence', vector_to_list(exs.map(transform))]);
322
  }
323

324
  function makeBlockIfNeeded(exs: Node[]): List {
325
    return hasDeclarationAtToplevel(exs)
30✔
326
      ? vector_to_list(['block', makeSequenceIfNeeded(exs)])
327
      : makeSequenceIfNeeded(exs);
328
  }
329

330
  if (program) {
68!
331
    return transform(program);
68✔
332
  } else {
333
    unreachable();
×
NEW
334
    throw new ParseError('Invalid parse', parse.name);
×
335
  }
336
}
337

338
/**
339
 * A wrapper around the Source Parser's `tokenize` function. Set `asRuntime` to `true`
340
 * if the function is meant to be called from Source code.
341
 */
342
export function tokenize(x: string, context: Context, asRuntime?: boolean): Value {
343
  try {
3✔
344
    const tokensArr = SourceParser.tokenize(x, context).map(tok => x.substring(tok.start, tok.end));
58✔
345
    return vector_to_list(tokensArr);
3✔
346
  } catch (error) {
347
    if (asRuntime) {
1!
348
      throw new ParseError(error.message, tokenize.name);
1✔
349
    }
350

NEW
351
    throw error;
×
352
  }
353
}
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