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

source-academy / js-slang / 24602591668

18 Apr 2026 10:20AM UTC coverage: 78.498% (+0.1%) from 78.391%
24602591668

Pull #1893

github

web-flow
Merge 781911aba into 8a89e808f
Pull Request #1893: Error Handling and Stringify Changes

3116 of 4188 branches covered (74.4%)

Branch coverage included in aggregate %.

787 of 960 new or added lines in 76 files covered. (81.98%)

20 existing lines in 11 files now uncovered.

7055 of 8769 relevant lines covered (80.45%)

176938.63 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
// function makeSequenceIfNeeded(this: TransformerData, exs: Node[]): List {
47
//   return exs.length === 1
48
//     ? this.transform(exs[0])
49
//     : vector_to_list(['sequence', vector_to_list(exs.map(this.transform))]);
50
// }
51

52
// function makeBlockIfNeeded(this: TransformerData, exs: Node[]): List {
53
//   return hasDeclarationAtToplevel(exs)
54
//     ? vector_to_list(['block', makeSequenceIfNeeded.call(this ,exs)])
55
//     : makeSequenceIfNeeded.call(this, exs);
56
// }
57

58
// checks if sequence has declaration at toplevel
59
// (outside of any block)
60
function hasDeclarationAtToplevel(exs: Node[]) {
61
  return exs.some(isDeclaration);
30✔
62
}
63

64
type ParseTransformer<T extends Node> = (this: TransformerData, node: T) => List;
65
type ASTTransformers = {
66
  [K in Node['type']]?: ParseTransformer<NodeTypeToNode<K>>;
67
};
68

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

287
    if (node.kind === 'let') {
28✔
288
      return vector_to_list(['variable_declaration', this.transform(id), this.transform(init)]);
15✔
289
    } else if (node.kind === 'const') {
13!
290
      return vector_to_list(['constant_declaration', this.transform(id), this.transform(init)]);
13✔
291
    } else {
292
      unreachable();
×
NEW
293
      throw new ParseError(
×
294
        `Invalid declaration kind for VariableDeclaration: ${node.kind}`,
295
        this.funcName,
296
        node,
297
      );
298
    }
299
  },
300
  WhileStatement({ test, body }) {
301
    return vector_to_list(['while_loop', this.transform(test), this.transform(body)]);
2✔
302
  },
303
};
304

305
export function parse(x: string, context: Context): Value {
306
  context.chapter = libraryParserLanguage;
71✔
307
  const program = sourceParse(x, context);
71✔
308
  if (context.errors.length > 0) {
71✔
309
    throw new ParseError(context.errors[0].explain(), parse.name);
3✔
310
  }
311

312
  function transform(node: Node) {
313
    if (!(node.type in transformers)) {
765!
NEW
314
      unreachable();
×
NEW
315
      throw new ParseError(`Cannot transform unknown node type: ${node.type}`, parse.name, node);
×
316
    }
317

318
    const transformer = transformers[node.type] as ParseTransformer<Node>;
765✔
319
    return transformer.call(
765✔
320
      {
321
        funcName: parse.name,
322
        transform,
323
        makeBlockIfNeeded,
324
        makeSequenceIfNeeded,
325
      },
326
      node,
327
    );
328
  }
329

330
  function makeSequenceIfNeeded(exs: Node[]): List {
331
    return exs.length === 1
99✔
332
      ? transform(exs[0])
333
      : vector_to_list(['sequence', vector_to_list(exs.map(transform))]);
334
  }
335

336
  function makeBlockIfNeeded(exs: Node[]): List {
337
    return hasDeclarationAtToplevel(exs)
30✔
338
      ? vector_to_list(['block', makeSequenceIfNeeded(exs)])
339
      : makeSequenceIfNeeded(exs);
340
  }
341

342
  if (program) {
68!
343
    return transform(program);
68✔
344
  } else {
345
    unreachable();
×
NEW
346
    throw new ParseError('Invalid parse', parse.name);
×
347
  }
348
}
349

350
/**
351
 * A wrapper around the Source Parser's `tokenize` function. Set `asRuntime` to `true`
352
 * if the function is meant to be called from Source code.
353
 */
354
export function tokenize(x: string, context: Context, asRuntime?: boolean): Value {
355
  try {
3✔
356
    const tokensArr = SourceParser.tokenize(x, context).map(tok => x.substring(tok.start, tok.end));
58✔
357
    return vector_to_list(tokensArr);
3✔
358
  } catch (error) {
359
    if (asRuntime) {
1!
360
      throw new ParseError(error.message, tokenize.name);
1✔
361
    }
362

NEW
363
    throw error;
×
364
  }
365
}
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