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

source-academy / py-slang / 23097039458

14 Mar 2026 09:47PM UTC coverage: 34.836% (-0.004%) from 34.84%
23097039458

push

github

web-flow
Various package improvements (#88)

* Add ESLint and Prettier + make formatting changes

* Add linting to the workflow

* Add format to CI

* Fix the remaining types + make Value types stricter + temporarily remove tests from ESLint

* Add prettierignore + Extend prettier to entire repository + fix ESLint not working on tests + Move linter + formatters to `devDependencies`

* Fix formatting of `eslint.config.mjs`

* update package-lock.json;

* adding back rollup-plugin-sourcemaps to fix build failure

* Restore Coveralls workflow

* Update .prettierrc

* Reformat

* Add missing newline at EOF

* Sort imports

* Add VSCode config

* Migrate to Yarn as package manager

* Update workflows to use Yarn

* Fix inconsistent versions

* Remove useless comment

* Clean up migrated Yarn lockfile

* Fix incorrect merge deleting coverage script

* Remove unnecessary flag

* Remove unnecessary items from prettierignore

* Add VSCode settings for NL at EOF

* Clean up tsconfig

* Add coverage to ESLint ignore

* Fix mistakes from previous merge resolution

* Clean up dependencies

* Switch to double quotes

* Revert "Switch to double quotes"

This reverts commit 830c26c72.

* Switch to double quotes properly

* Undo ESLint fixes temporarily

* Silence lint for now

---------

Co-authored-by: Lucas Ng <152241991+lucasnyc@users.noreply.github.com>
Co-authored-by: Lucas <nyc@yuchengng.com>
Co-authored-by: Richard Dominick <34370238+RichDom2185@users.noreply.github.com>

267 of 1065 branches covered (25.07%)

Branch coverage included in aggregate %.

877 of 2551 new or added lines in 20 files covered. (34.38%)

15 existing lines in 5 files now uncovered.

1124 of 2928 relevant lines covered (38.39%)

83.43 hits per line

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

77.42
/src/parser/parser.ts
1
/*
2
* Full disclosure: some of the functions and general layout of the file is
3
* from my own implementation of a parser
4
* in Rust.
5
* https://github.com/Fidget-Spinner/crafting_interpreters/blob/main/rust/src/parser.rs
6
*
7
* That is in turn an implementation of the book "Crafting Interpreters" by
8
* Robert Nystrom, which implements an interpreter in Java.
9
* https://craftinginterpreters.com/parsing-expressions.html.
10
* I've included the MIT license that code snippets from
11
* the book is licensed under down below. See
12
* https://github.com/munificent/craftinginterpreters/blob/master/LICENSE
13
*
14
*
15
* My changes:
16
*   - The book was written in Java. I have written this in TypeScript.
17
*   - My Rust implementation uses pattern matching, but the visitor pattern is
18
*     used here.
19
*   - Additionally, the production rules are completely different
20
*     from the book as a whole different language is being parsed.
21
*
22
*
23
    Permission is hereby granted, free of charge, to any person obtaining a copy
24
    of this software and associated documentation files (the "Software"), to
25
    deal in the Software without restriction, including without limitation the
26
    rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
27
    sell copies of the Software, and to permit persons to whom the Software is
28
    furnished to do so, subject to the following conditions:
29

30
    The above copyright notice and this permission notice shall be included in
31
    all copies or substantial portions of the Software.
32

33
    THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
34
    IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
35
    FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
36
    AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
37
    LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
38
    FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
39
    IN THE SOFTWARE.
40
**/
41

42
import { ExprNS, FunctionParam, StmtNS } from "../ast-types";
2✔
43
import { SPECIAL_IDENTIFIER_TOKENS, Token } from "../tokenizer/tokenizer";
2✔
44
import { TokenType } from "../tokens";
2✔
45
import { ParserErrors } from "./errors";
2✔
46

47
type Expr = ExprNS.Expr;
48
type Stmt = StmtNS.Stmt;
49

50
const PSEUD_NAMES = [TokenType.TRUE, TokenType.FALSE, TokenType.NONE];
2✔
51

52
export class Parser {
2✔
53
  private readonly source: string;
54
  private readonly tokens: Token[];
55
  private current: number;
56

57
  constructor(source: string, tokens: Token[]) {
58
    this.source = source;
99✔
59
    this.tokens = tokens;
99✔
60
    this.current = 0;
99✔
61
  }
62

63
  // Consumes tokens while tokenTypes matches.
64
  private match(...tokenTypes: TokenType[]): boolean {
65
    for (const tokenType of tokenTypes) {
7,539✔
66
      if (this.check(tokenType)) {
11,959✔
67
        this.advance();
700✔
68
        return true;
700✔
69
      }
70
    }
71
    return false;
6,839✔
72
  }
73

74
  private check(...type: TokenType[]): boolean {
75
    if (this.isAtEnd()) {
13,713!
NEW
76
      return false;
×
77
    }
78
    for (const tokenType of type) {
13,713✔
79
      if (this.peek().type === tokenType) {
16,592✔
80
        return true;
1,576✔
81
      }
82
    }
83
    return false;
12,137✔
84
  }
85

86
  private advance(): Token {
87
    if (!this.isAtEnd()) {
1,176✔
88
      this.current += 1;
1,176✔
89
    }
90
    return this.previous();
1,176✔
91
  }
92

93
  private isAtEnd(): boolean {
94
    return this.peek().type === TokenType.ENDMARKER;
15,146✔
95
  }
96

97
  private peek(): Token {
98
    return this.tokens[this.current];
35,551✔
99
  }
100

101
  private previous(): Token {
102
    return this.tokens[this.current - 1];
2,272✔
103
  }
104

105
  private consume(type: TokenType, message: string): Token {
106
    if (this.check(type)) return this.advance();
410✔
NEW
107
    const token = this.tokens[this.current];
×
NEW
108
    throw new ParserErrors.ExpectedTokenError(this.source, token, message);
×
109
  }
110

111
  private synchronize() {
NEW
112
    this.advance();
×
NEW
113
    while (!this.isAtEnd()) {
×
NEW
114
      if (this.match(TokenType.NEWLINE)) {
×
UNCOV
115
        return false;
×
116
      }
NEW
117
      if (
×
118
        this.match(
119
          TokenType.FOR,
120
          TokenType.WHILE,
121
          TokenType.DEF,
122
          TokenType.IF,
123
          TokenType.ELIF,
124
          TokenType.ELSE,
125
          TokenType.RETURN,
126
        )
127
      ) {
NEW
128
        return true;
×
129
      }
NEW
130
      this.advance();
×
131
    }
NEW
132
    return false;
×
133
  }
134

135
  parse(): Stmt {
136
    return this.file_input();
99✔
137
    // return this.expression();
138
  }
139

140
  //// THE NAMES OF THE FOLLOWING FUNCTIONS FOLLOW THE PRODUCTION RULES IN THE GRAMMAR.
141
  //// HENCE THEIR NAMES MIGHT NOT BE COMPLIANT WITH CAMELCASE
142
  private file_input(): Stmt {
143
    const startToken = this.peek();
99✔
144
    const statements: Stmt[] = [];
99✔
145
    while (!this.isAtEnd()) {
99✔
146
      if (this.match(TokenType.NEWLINE) || this.match(TokenType.DEDENT)) {
158✔
147
        continue;
24✔
148
      }
149
      statements.push(this.stmt());
134✔
150
    }
151
    const endToken = this.peek();
99✔
152
    return new StmtNS.FileInput(startToken, endToken, statements, []);
99✔
153
  }
154

155
  private stmt(): Stmt {
156
    if (this.check(TokenType.DEF, TokenType.FOR, TokenType.IF, TokenType.WHILE)) {
200✔
157
      return this.compound_stmt();
30✔
158
    } else if (
170✔
159
      this.check(
160
        TokenType.NAME,
161
        ...PSEUD_NAMES,
162
        TokenType.NUMBER,
163
        TokenType.PASS,
164
        TokenType.BREAK,
165
        TokenType.CONTINUE,
166
        TokenType.MINUS,
167
        TokenType.PLUS,
168
        TokenType.INDENT,
169
        TokenType.DEDENT,
170
        TokenType.RETURN,
171
        TokenType.FROM,
172
        TokenType.GLOBAL,
173
        TokenType.NONLOCAL,
174
        TokenType.ASSERT,
175
        TokenType.LPAR,
176
        TokenType.LSQB,
177
        TokenType.STRING,
178
        TokenType.BIGINT,
179
        ...SPECIAL_IDENTIFIER_TOKENS,
180
      )
181
    ) {
182
      return this.simple_stmt();
170✔
183
    }
NEW
184
    const startToken = this.peek();
×
NEW
185
    const endToken = this.synchronize() ? this.previous() : this.peek();
×
NEW
186
    try {
×
NEW
187
      this.parse_invalid(startToken, endToken);
×
188
    } catch (e) {
NEW
189
      if (e instanceof ParserErrors.BaseParserError) {
×
NEW
190
        throw e;
×
191
      }
192
    }
NEW
193
    throw new ParserErrors.GenericUnexpectedSyntaxError(
×
194
      startToken.line,
195
      startToken.col,
196
      this.source,
197
      startToken.indexInSource,
198
      endToken.indexInSource,
199
    );
200
  }
201

202
  private compound_stmt(): Stmt {
203
    if (this.match(TokenType.IF)) {
30✔
204
      return this.if_stmt();
3✔
205
    } else if (this.match(TokenType.WHILE)) {
27✔
206
      return this.while_stmt();
1✔
207
    } else if (this.match(TokenType.FOR)) {
26✔
208
      return this.for_stmt();
1✔
209
    } else if (this.match(TokenType.DEF)) {
25✔
210
      return this.funcdef();
25✔
211
    }
NEW
212
    throw new Error("Unreachable code path");
×
213
  }
214

215
  private if_stmt(): Stmt {
216
    const startToken = this.previous();
6✔
217
    const start = this.previous();
6✔
218
    const cond = this.test();
6✔
219
    this.consume(TokenType.COLON, "Expected ':' after if");
6✔
220
    const block = this.suite();
6✔
221
    let elseStmt = null;
6✔
222
    if (this.match(TokenType.ELIF)) {
6✔
223
      elseStmt = [this.if_stmt()];
3✔
224
    } else if (this.match(TokenType.ELSE)) {
3!
225
      this.consume(TokenType.COLON, "Expect ':' after else");
3✔
226
      elseStmt = this.suite();
3✔
227
    } else {
NEW
228
      throw new ParserErrors.NoElseBlockError(this.source, start);
×
229
    }
230
    const endToken = this.previous();
6✔
231
    return new StmtNS.If(startToken, endToken, cond, block, elseStmt);
6✔
232
  }
233

234
  private while_stmt(): Stmt {
235
    const startToken = this.peek();
1✔
236
    const cond = this.test();
1✔
237
    this.consume(TokenType.COLON, "Expected ':' after while");
1✔
238
    const block = this.suite();
1✔
239
    const endToken = this.previous();
1✔
240
    return new StmtNS.While(startToken, endToken, cond, block);
1✔
241
  }
242

243
  private for_stmt(): Stmt {
244
    const startToken = this.peek();
1✔
245
    const target = this.advance();
1✔
246
    this.consume(TokenType.IN, "Expected in after for");
1✔
247
    const iter = this.test();
1✔
248
    this.consume(TokenType.COLON, "Expected ':' after for");
1✔
249
    const block = this.suite();
1✔
250
    const endToken = this.previous();
1✔
251
    return new StmtNS.For(startToken, endToken, target, iter, block);
1✔
252
  }
253

254
  private funcdef(): Stmt {
255
    const startToken = this.peek();
25✔
256
    const name = this.advance();
25✔
257
    const args = this.parameters();
25✔
258
    this.consume(TokenType.COLON, "Expected ':' after def");
25✔
259
    const block = this.suite();
25✔
260
    const endToken = this.previous();
25✔
261
    return new StmtNS.FunctionDef(startToken, endToken, name, args, block, []);
25✔
262
  }
263

264
  private simple_stmt(): Stmt {
265
    const startToken = this.peek();
170✔
266
    let res = null;
170✔
267
    if (this.match(TokenType.INDENT)) {
170!
NEW
268
      res = new StmtNS.Indent(startToken, startToken);
×
269
    } else if (this.match(TokenType.DEDENT)) {
170!
NEW
270
      res = new StmtNS.Dedent(startToken, startToken);
×
271
    } else if (this.match(TokenType.PASS)) {
170✔
272
      res = new StmtNS.Pass(startToken, startToken);
9✔
273
    } else if (this.match(TokenType.BREAK)) {
161!
NEW
274
      res = new StmtNS.Break(startToken, startToken);
×
275
    } else if (this.match(TokenType.CONTINUE)) {
161!
NEW
276
      res = new StmtNS.Continue(startToken, startToken);
×
277
    } else if (this.match(TokenType.RETURN)) {
161✔
278
      res = new StmtNS.Return(
20✔
279
        startToken,
280
        startToken,
281
        this.check(TokenType.NEWLINE) ? null : this.test(),
20!
282
      );
283
    } else if (this.match(TokenType.FROM)) {
141✔
284
      res = this.import_from();
5✔
285
    } else if (this.match(TokenType.GLOBAL)) {
136!
NEW
286
      res = new StmtNS.Global(startToken, startToken, this.advance());
×
287
    } else if (this.match(TokenType.NONLOCAL)) {
136✔
288
      res = new StmtNS.NonLocal(startToken, startToken, this.advance());
5✔
289
    } else if (this.match(TokenType.ASSERT)) {
131!
NEW
290
      res = new StmtNS.Assert(startToken, startToken, this.test());
×
291
    } else if (
131!
292
      this.check(
293
        TokenType.NAME,
294
        TokenType.LPAR,
295
        TokenType.LSQB,
296
        TokenType.NUMBER,
297
        TokenType.STRING,
298
        TokenType.BIGINT,
299
        TokenType.MINUS,
300
        TokenType.PLUS,
301
        ...SPECIAL_IDENTIFIER_TOKENS,
302
      )
303
    ) {
304
      const expr = this.test();
131✔
305

306
      if (this.check(TokenType.COLON)) {
131!
NEW
307
        if (!(expr instanceof ExprNS.Variable)) {
×
NEW
308
          throw new ParserErrors.InvalidAssignmentError(this.source, startToken);
×
309
        }
NEW
310
        this.advance();
×
NEW
311
        const ann = this.test();
×
NEW
312
        this.consume(TokenType.EQUAL, "Expect equal in annotated assignment");
×
NEW
313
        const value = this.test();
×
NEW
314
        res = new StmtNS.AnnAssign(startToken, this.previous(), expr, value, ann);
×
315
      } else if (this.check(TokenType.EQUAL)) {
131✔
316
        if (!(expr instanceof ExprNS.Variable || expr instanceof ExprNS.Subscript)) {
30!
NEW
317
          throw new ParserErrors.InvalidAssignmentError(this.source, startToken);
×
318
        }
319
        this.advance();
30✔
320
        const value = this.test();
30✔
321
        res = new StmtNS.Assign(startToken, this.previous(), expr, value);
30✔
322
      } else {
323
        res = new StmtNS.SimpleExpr(startToken, this.previous(), expr);
101✔
324
      }
325
      // res = new StmtNS.SimpleExpr(startToken, startToken, expr);
326
    } else {
NEW
327
      throw new Error("Unreachable code path");
×
328
    }
329
    this.consume(TokenType.NEWLINE, "Expected newline");
170✔
330
    return res;
170✔
331
  }
332

333
  private import_from(): Stmt {
334
    const startToken = this.previous();
5✔
335
    const module = this.advance();
5✔
336
    this.consume(TokenType.IMPORT, "Expected import keyword");
5✔
337

338
    const names: Token[] = [];
5✔
339
    let useParens = false;
5✔
340

341
    if (this.check(TokenType.LPAR)) {
5✔
342
      this.consume(TokenType.LPAR, "Expected '(' after import");
3✔
343
      useParens = true;
3✔
344
    }
345

346
    names.push(this.consume(TokenType.NAME, "Expected name to import"));
5✔
347
    while (this.match(TokenType.COMMA)) {
5✔
348
      names.push(this.consume(TokenType.NAME, "Expected name after comma"));
4✔
349
    }
350

351
    if (useParens) {
5✔
352
      this.consume(TokenType.RPAR, "Expected ')' after import");
3✔
353
    }
354

355
    return new StmtNS.FromImport(startToken, this.previous(), module, names);
5✔
356
  }
357

358
  private parameters(): FunctionParam[] {
359
    this.consume(TokenType.LPAR, "Expected opening parentheses");
25✔
360
    const res = this.varparamslist();
25✔
361
    this.consume(TokenType.RPAR, "Expected closing parentheses");
25✔
362
    return res;
25✔
363
  }
364

365
  private test(): Expr {
366
    if (this.match(TokenType.LAMBDA)) {
262✔
367
      return this.lambdef();
7✔
368
    } else {
369
      const startToken = this.peek();
255✔
370
      const consequent = this.or_test();
255✔
371
      if (this.match(TokenType.IF)) {
255✔
372
        const predicate = this.or_test();
4✔
373
        this.consume(TokenType.ELSE, "Expected else");
4✔
374
        const alternative = this.test();
4✔
375
        return new ExprNS.Ternary(startToken, this.previous(), predicate, consequent, alternative);
4✔
376
      }
377
      return consequent;
251✔
378
    }
379
  }
380

381
  private lambdef(): Expr {
382
    const startToken = this.previous();
7✔
383
    const args = this.varparamslist();
7✔
384
    if (this.match(TokenType.COLON)) {
7!
385
      const test = this.test();
7✔
386
      return new ExprNS.Lambda(startToken, this.previous(), args, test);
7✔
NEW
387
    } else if (this.match(TokenType.DOUBLECOLON)) {
×
NEW
388
      const block = this.suite();
×
NEW
389
      return new ExprNS.MultiLambda(startToken, this.previous(), args, block, []);
×
390
    }
NEW
391
    this.consume(TokenType.COLON, "Expected ':' after lambda");
×
NEW
392
    throw new Error("unreachable code path");
×
393
  }
394

395
  private suite(): Stmt[] {
396
    const stmts = [];
36✔
397
    if (this.match(TokenType.NEWLINE)) {
36✔
398
      this.consume(TokenType.INDENT, "Expected indent");
36✔
399
      while (!this.match(TokenType.DEDENT)) {
36✔
400
        stmts.push(this.stmt());
66✔
401
      }
402
    }
403
    return stmts;
36✔
404
  }
405

406
  private varparamslist(): FunctionParam[] {
407
    const params = [];
32✔
408
    while (!this.check(TokenType.COLON) && !this.check(TokenType.RPAR)) {
32✔
409
      if (this.match(TokenType.STAR)) {
22!
NEW
410
        const name = this.consume(
×
411
          TokenType.NAME,
412
          "Expected a proper identifier after * in parameter",
413
        );
NEW
414
        params.push({ ...name, isStarred: true });
×
415
      } else {
416
        const name = this.consume(TokenType.NAME, "Expected a proper identifier in parameter");
22✔
417
        params.push({ ...name, isStarred: false });
22✔
418
      }
419
      if (!this.match(TokenType.COMMA)) {
22✔
420
        break;
13✔
421
      }
422
    }
423
    return params;
32✔
424
  }
425

426
  private or_test(): Expr {
427
    const startToken = this.peek();
259✔
428
    let expr = this.and_test();
259✔
429
    while (this.match(TokenType.OR)) {
259✔
430
      const operator = this.previous();
6✔
431
      const right = this.and_test();
6✔
432
      expr = new ExprNS.BoolOp(startToken, this.previous(), expr, operator, right);
6✔
433
    }
434
    return expr;
259✔
435
  }
436

437
  private and_test(): Expr {
438
    const startToken = this.peek();
265✔
439
    let expr = this.not_test();
265✔
440
    while (this.match(TokenType.AND)) {
265✔
441
      const operator = this.previous();
8✔
442
      const right = this.not_test();
8✔
443
      expr = new ExprNS.BoolOp(startToken, this.previous(), expr, operator, right);
8✔
444
    }
445
    return expr;
265✔
446
  }
447

448
  private not_test(): Expr {
449
    const startToken = this.peek();
277✔
450
    if (this.match(TokenType.NOT, TokenType.BANG)) {
277✔
451
      const operator = this.previous();
4✔
452
      return new ExprNS.Unary(startToken, this.previous(), operator, this.not_test());
4✔
453
    }
454
    return this.comparison();
273✔
455
  }
456

457
  private comparison(): Expr {
458
    const startToken = this.peek();
273✔
459
    let expr = this.arith_expr();
273✔
460
    // @TODO: Add the rest of the comparisons
461
    while (
273✔
462
      this.match(
463
        TokenType.LESS,
464
        TokenType.GREATER,
465
        TokenType.DOUBLEEQUAL,
466
        TokenType.GREATEREQUAL,
467
        TokenType.LESSEQUAL,
468
        TokenType.NOTEQUAL,
469
        TokenType.IS,
470
        TokenType.ISNOT,
471
        TokenType.IN,
472
        TokenType.NOTIN,
473
      )
474
    ) {
475
      const operator = this.previous();
20✔
476
      const right = this.arith_expr();
20✔
477
      expr = new ExprNS.Compare(startToken, this.previous(), expr, operator, right);
20✔
478
    }
479
    return expr;
273✔
480
  }
481

482
  private arith_expr(): Expr {
483
    const startToken = this.peek();
293✔
484
    let expr = this.term();
293✔
485
    while (this.match(TokenType.PLUS, TokenType.MINUS)) {
293✔
486
      const token = this.previous();
33✔
487
      const right = this.term();
33✔
488
      expr = new ExprNS.Binary(startToken, this.previous(), expr, token, right);
33✔
489
    }
490
    return expr;
293✔
491
  }
492

493
  private term(): Expr {
494
    const startToken = this.peek();
326✔
495
    let expr = this.factor();
326✔
496
    while (this.match(TokenType.STAR, TokenType.SLASH, TokenType.PERCENT, TokenType.DOUBLESLASH)) {
326✔
497
      const token = this.previous();
9✔
498
      const right = this.factor();
9✔
499
      expr = new ExprNS.Binary(startToken, this.previous(), expr, token, right);
9✔
500
    }
501
    return expr;
326✔
502
  }
503

504
  private factor(): Expr {
505
    const startToken = this.peek();
340✔
506
    if (this.match(TokenType.PLUS, TokenType.MINUS)) {
340✔
507
      const op = this.previous();
1✔
508
      const factor = this.factor();
1✔
509
      const endToken = this.previous();
1✔
510
      return new ExprNS.Unary(startToken, endToken, op, factor);
1✔
511
    }
512
    return this.power();
339✔
513
  }
514

515
  private power(): Expr {
516
    const startToken = this.peek();
339✔
517
    const expr = this.atom_expr();
339✔
518
    if (this.match(TokenType.DOUBLESTAR)) {
339✔
519
      const token = this.previous();
4✔
520
      const right = this.factor();
4✔
521
      const endToken = this.previous();
4✔
522
      return new ExprNS.Binary(startToken, endToken, expr, token, right);
4✔
523
    }
524
    return expr;
335✔
525
  }
526

527
  private atom_expr(): Expr {
528
    let startToken = this.peek();
339✔
529
    let ato = this.atom();
339✔
530
    while (this.check(TokenType.LPAR, TokenType.LSQB)) {
339✔
531
      if (this.match(TokenType.LPAR)) {
61!
532
        const args = this.arglist();
61✔
533
        const endToken = this.previous();
61✔
534
        ato = new ExprNS.Call(startToken, endToken, ato, args);
61✔
NEW
535
      } else if (this.match(TokenType.LSQB)) {
×
NEW
536
        const index = this.test();
×
UNCOV
537
        const endToken = this.previous();
×
UNCOV
538
        this.consume(TokenType.RSQB, "Expected closing ']'");
×
NEW
539
        ato = new ExprNS.Subscript(startToken, endToken, ato, index);
×
540
      }
541
      startToken = this.peek();
61✔
542
    }
543
    return ato;
339✔
544
  }
545

546
  private arglist(): Expr[] {
547
    const args = [];
61✔
548
    while (!this.check(TokenType.RPAR)) {
61✔
549
      const startToken = this.peek();
52✔
550
      if (this.match(TokenType.STAR)) {
52!
NEW
551
        const arg = this.test();
×
NEW
552
        args.push(new ExprNS.Starred(startToken, this.previous(), arg));
×
553
      } else {
554
        args.push(this.test());
52✔
555
      }
556
      if (!this.match(TokenType.COMMA)) {
52✔
557
        break;
39✔
558
      }
559
    }
560
    this.consume(TokenType.RPAR, "Expected closing ')' after function application");
61✔
561
    return args;
61✔
562
  }
563

564
  private list_expr(): Expr[] {
NEW
565
    const elements: Expr[] = [];
×
NEW
566
    while (!this.check(TokenType.RSQB)) {
×
NEW
567
      const element = this.test();
×
NEW
568
      elements.push(element);
×
NEW
569
      if (!this.match(TokenType.COMMA)) {
×
NEW
570
        break;
×
571
      }
572
    }
NEW
573
    this.consume(TokenType.RSQB, "Expected closing ']'");
×
NEW
574
    return elements;
×
575
  }
576

577
  private atom(): Expr {
578
    const startToken = this.peek();
339✔
579
    if (this.match(TokenType.TRUE)) return new ExprNS.Literal(startToken, this.previous(), true);
339✔
580
    if (this.match(TokenType.FALSE)) return new ExprNS.Literal(startToken, this.previous(), false);
337✔
581
    if (this.match(TokenType.NONE)) return new ExprNS.None(startToken, this.previous());
336✔
582
    if (this.match(TokenType.STRING)) {
332✔
583
      return new ExprNS.Literal(startToken, this.previous(), this.previous().lexeme);
22✔
584
    }
585
    if (this.match(TokenType.NUMBER)) {
310✔
586
      return new ExprNS.Literal(
10✔
587
        startToken,
588
        this.previous(),
589
        Number(this.previous().lexeme.replace(/_/g, "")),
590
      );
591
    }
592
    if (this.match(TokenType.BIGINT)) {
300✔
593
      return new ExprNS.BigIntLiteral(startToken, this.previous(), this.previous().lexeme);
144✔
594
    }
595
    if (this.match(TokenType.COMPLEX)) {
156✔
596
      return new ExprNS.Complex(startToken, this.previous(), this.previous().lexeme);
11✔
597
    }
598

599
    if (this.match(TokenType.NAME, ...PSEUD_NAMES)) {
145✔
600
      return new ExprNS.Variable(startToken, this.previous(), this.previous());
135✔
601
    }
602

603
    if (this.match(TokenType.LPAR)) {
10✔
604
      const expr = this.test();
10✔
605
      this.consume(TokenType.RPAR, "Expected closing ')'");
10✔
606
      return new ExprNS.Grouping(startToken, this.previous(), expr);
10✔
607
    }
608

NEW
609
    if (this.match(TokenType.LSQB)) {
×
NEW
610
      const elements = this.list_expr();
×
NEW
611
      return new ExprNS.List(startToken, this.previous(), elements);
×
612
    }
NEW
613
    const startTokenInvalid = this.peek();
×
NEW
614
    this.synchronize();
×
NEW
615
    const endTokenInvalid = this.peek();
×
NEW
616
    throw new ParserErrors.GenericUnexpectedSyntaxError(
×
617
      startToken.line,
618
      startToken.col,
619
      this.source,
620
      startTokenInvalid.indexInSource,
621
      endTokenInvalid.indexInSource,
622
    );
623
  }
624

625
  //// INVALID RULES
626
  private parse_invalid(startToken: Token, endToken: Token) {
627
    // @TODO invalid rules
628
  }
629
}
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