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

source-academy / py-slang / 23085365363

14 Mar 2026 09:37AM UTC coverage: 33.992% (-0.8%) from 34.84%
23085365363

Pull #88

github

web-flow
Merge 575ceae97 into d09c6d5b1
Pull Request #88: Various package improvements

387 of 1518 branches covered (25.49%)

Branch coverage included in aggregate %.

902 of 2566 new or added lines in 17 files covered. (35.15%)

11 existing lines in 4 files now uncovered.

1109 of 2883 relevant lines covered (38.47%)

169.44 hits per line

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

78.92
/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';
4✔
43
import { SPECIAL_IDENTIFIER_TOKENS, Token } from '../tokenizer/tokenizer';
4✔
44
import { TokenType } from '../tokens';
4✔
45
import { ParserErrors } from './errors';
4✔
46

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

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

52
export class Parser {
4✔
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;
198✔
59
    this.tokens = tokens;
198✔
60
    this.current = 0;
198✔
61
  }
62

63
  // Consumes tokens while tokenTypes matches.
64
  private match(...tokenTypes: TokenType[]): boolean {
65
    for (const tokenType of tokenTypes) {
15,078✔
66
      if (this.check(tokenType)) {
23,918✔
67
        this.advance();
1,400✔
68
        return true;
1,400✔
69
      }
70
    }
71
    return false;
13,678✔
72
  }
73

74
  private check(...type: TokenType[]): boolean {
75
    if (this.isAtEnd()) {
27,426!
NEW
76
      return false;
×
77
    }
78
    for (const tokenType of type) {
27,426✔
79
      if (this.peek().type === tokenType) {
33,184✔
80
        return true;
3,152✔
81
      }
82
    }
83
    return false;
24,274✔
84
  }
85

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

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

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

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

105
  private consume(type: TokenType, message: string): Token {
106
    if (this.check(type)) return this.advance();
820✔
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();
198✔
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();
198✔
144
    const statements: Stmt[] = [];
198✔
145
    while (!this.isAtEnd()) {
198✔
146
      if (this.match(TokenType.NEWLINE) || this.match(TokenType.DEDENT)) {
316✔
147
        continue;
48✔
148
      }
149
      statements.push(this.stmt());
268✔
150
    }
151
    const endToken = this.peek();
198✔
152
    return new StmtNS.FileInput(startToken, endToken, statements, []);
198✔
153
  }
154

155
  private stmt(): Stmt {
156
    if (this.check(TokenType.DEF, TokenType.FOR, TokenType.IF, TokenType.WHILE)) {
400✔
157
      return this.compound_stmt();
60✔
158
    } else if (
340✔
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();
340✔
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
    );
199
  }
200

201
  private compound_stmt(): Stmt {
202
    if (this.match(TokenType.IF)) {
60✔
203
      return this.if_stmt();
6✔
204
    } else if (this.match(TokenType.WHILE)) {
54✔
205
      return this.while_stmt();
2✔
206
    } else if (this.match(TokenType.FOR)) {
52✔
207
      return this.for_stmt();
2✔
208
    } else if (this.match(TokenType.DEF)) {
50✔
209
      return this.funcdef();
50✔
210
    }
NEW
211
    throw new Error('Unreachable code path');
×
212
  }
213

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

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

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

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

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

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

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

337
    const names: Token[] = [];
10✔
338
    let useParens = false;
10✔
339

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

345
    names.push(this.consume(TokenType.NAME, 'Expected name to import'));
10✔
346
    while (this.match(TokenType.COMMA)) {
10✔
347
      names.push(this.consume(TokenType.NAME, 'Expected name after comma'));
8✔
348
    }
349

350
    if (useParens) {
10✔
351
      this.consume(TokenType.RPAR, "Expected ')' after import");
6✔
352
    }
353

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

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

364
  private test(): Expr {
365
    if (this.match(TokenType.LAMBDA)) {
524✔
366
      return this.lambdef();
14✔
367
    } else {
368
      const startToken = this.peek();
510✔
369
      const consequent = this.or_test();
510✔
370
      if (this.match(TokenType.IF)) {
510✔
371
        const predicate = this.or_test();
8✔
372
        this.consume(TokenType.ELSE, 'Expected else');
8✔
373
        const alternative = this.test();
8✔
374
        return new ExprNS.Ternary(startToken, this.previous(), predicate, consequent, alternative);
8✔
375
      }
376
      return consequent;
502✔
377
    }
378
  }
379

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

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

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

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

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

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

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

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

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

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

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

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

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

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

576
  private atom(): Expr {
577
    const startToken = this.peek();
678✔
578
    if (this.match(TokenType.TRUE)) return new ExprNS.Literal(startToken, this.previous(), true);
678✔
579
    if (this.match(TokenType.FALSE)) return new ExprNS.Literal(startToken, this.previous(), false);
674✔
580
    if (this.match(TokenType.NONE)) return new ExprNS.None(startToken, this.previous());
672✔
581
    if (this.match(TokenType.STRING)) {
664✔
582
      return new ExprNS.Literal(startToken, this.previous(), this.previous().lexeme);
44✔
583
    }
584
    if (this.match(TokenType.NUMBER)) {
620✔
585
      return new ExprNS.Literal(
20✔
586
        startToken,
587
        this.previous(),
588
        Number(this.previous().lexeme.replace(/_/g, '')),
589
      );
590
    }
591
    if (this.match(TokenType.BIGINT)) {
600✔
592
      return new ExprNS.BigIntLiteral(startToken, this.previous(), this.previous().lexeme);
288✔
593
    }
594
    if (this.match(TokenType.COMPLEX)) {
312✔
595
      return new ExprNS.Complex(startToken, this.previous(), this.previous().lexeme);
22✔
596
    }
597

598
    if (this.match(TokenType.NAME, ...PSEUD_NAMES)) {
290✔
599
      return new ExprNS.Variable(startToken, this.previous(), this.previous());
270✔
600
    }
601

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

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

622
  //// INVALID RULES
623
  private parse_invalid(_startToken: Token, _endToken: Token) {
624
    // @TODO invalid rules
625
  }
626
}
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