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

RauliL / laskin.js / 7089912914

04 Dec 2023 04:58PM UTC coverage: 74.955% (+7.4%) from 67.583%
7089912914

push

github

RauliL
Release changes as v1.3.0

434 of 521 branches covered (0.0%)

Branch coverage included in aggregate %.

1218 of 1683 relevant lines covered (72.37%)

18.88 hits per line

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

90.91
/src/parser.ts
1
import {
2
  DefinitionNode,
3
  LiteralNode,
4
  Node,
5
  RecordLiteralNode,
6
  SymbolNode,
7
  VectorLiteralNode,
8
} from "./ast";
9
import { ScriptedQuote } from "./quote";
10✔
10
import { SyntaxError } from "./exception";
10✔
11
import { StringValue, newQuoteValue, newStringValue } from "./value";
10✔
12

13
const isSpace = /^\s$/;
10✔
14
const isSymbol = /^[^[\](){},\s]$/u;
10✔
15

16
class Parser {
17
  private readonly source: string;
18
  private offset: number;
19
  private line: number;
20
  private column: number;
21

22
  public constructor(source: string, line: number, column: number) {
23
    this.source = source;
86✔
24
    this.offset = 0;
86✔
25
    this.line = line;
86✔
26
    this.column = column;
86✔
27
  }
28

29
  public parseScript(): Node[] {
30
    const nodes: Node[] = [];
86✔
31

32
    for (;;) {
86✔
33
      this.skipWhitespace();
158✔
34
      if (this.eof()) {
158✔
35
        break;
58✔
36
      }
37
      nodes.push(this.parseStatement());
100✔
38
    }
39

40
    return nodes;
58✔
41
  }
42

43
  private parseStatement(): Node {
44
    this.skipWhitespace();
108✔
45

46
    switch (this.source[this.offset]) {
108✔
47
      case "(":
48
        return this.parseQuoteLiteral();
10✔
49

50
      case "[":
51
        return this.parseVectorLiteral();
14✔
52

53
      case "{":
54
        return this.parseRecordLiteral();
16✔
55

56
      case '"':
57
      case "'":
58
        return this.parseStringLiteral();
42✔
59

60
      default:
61
        return this.parseStatementSymbol();
26✔
62
    }
63
  }
64

65
  private parseExpression(): Node {
66
    this.skipWhitespace();
18✔
67

68
    if (this.eof()) {
18!
69
      throw new SyntaxError("Unexpected end of input; Missing expression.", {
×
70
        line: this.line,
71
        column: this.column,
72
      });
73
    }
74

75
    switch (this.source[this.offset]) {
18!
76
      case "(":
77
        return this.parseQuoteLiteral();
×
78

79
      case "[":
80
        return this.parseVectorLiteral();
×
81

82
      case "{":
83
        return this.parseRecordLiteral();
×
84

85
      case '"':
86
      case "'":
87
        return this.parseStringLiteral();
×
88

89
      default:
90
        return this.parseSymbol();
18✔
91
    }
92
  }
93

94
  private parseQuoteLiteral(): LiteralNode {
95
    const nodes: Node[] = [];
10✔
96

97
    this.skipWhitespace();
10✔
98

99
    const line = this.line;
10✔
100
    const column = this.column;
10✔
101

102
    // Skip "(" and excess whitespace.
103
    this.read();
10✔
104
    this.skipWhitespace();
10✔
105

106
    if (!this.peekRead(")")) {
10✔
107
      for (;;) {
6✔
108
        if (this.eof()) {
14✔
109
          throw new SyntaxError("Unterminated quote literal: Missing `)'.", {
4✔
110
            line,
111
            column,
112
          });
113
        } else if (this.peekRead(")")) {
10✔
114
          break;
2✔
115
        }
116
        nodes.push(this.parseStatement());
8✔
117
        this.skipWhitespace();
8✔
118
      }
119
    }
120

121
    return {
6✔
122
      type: "Literal",
123
      position: { line, column },
124
      value: newQuoteValue(new ScriptedQuote(nodes)),
125
    };
126
  }
127

128
  private parseVectorLiteral(): VectorLiteralNode {
129
    const elements: Node[] = [];
14✔
130

131
    this.skipWhitespace();
14✔
132

133
    const line = this.line;
14✔
134
    const column = this.column;
14✔
135

136
    // Skip "[" and excess whitespace.
137
    this.read();
14✔
138
    this.skipWhitespace();
14✔
139

140
    if (!this.peekRead("]")) {
14✔
141
      for (;;) {
8✔
142
        if (this.eof()) {
14✔
143
          throw new SyntaxError("Unterminated vector literal: Missing `]'.", {
2✔
144
            line,
145
            column,
146
          });
147
        } else if (this.peekRead("]")) {
12✔
148
          break;
2✔
149
        }
150
        elements.push(this.parseExpression());
10✔
151
        this.skipWhitespace();
10✔
152
        if (this.peekRead(",")) {
10✔
153
          continue;
6✔
154
        } else if (this.peekRead("]")) {
4✔
155
          break;
2✔
156
        }
157

158
        throw new SyntaxError("Unterminated vector literal: Missing `]'.", {
2✔
159
          line,
160
          column,
161
        });
162
      }
163
    }
164

165
    return {
10✔
166
      type: "VectorLiteral",
167
      position: { line, column },
168
      elements,
169
    };
170
  }
171

172
  private parseRecordLiteral(): RecordLiteralNode {
173
    const elements = new Map<string, Node>();
16✔
174

175
    this.skipWhitespace();
16✔
176

177
    const line = this.line;
16✔
178
    const column = this.column;
16✔
179

180
    // Skip "{" and excess whitespace.
181
    this.read();
16✔
182
    this.skipWhitespace();
16✔
183

184
    if (!this.peekRead("}")) {
16✔
185
      for (;;) {
10✔
186
        if (this.eof()) {
14✔
187
          throw new SyntaxError("Unterminated record literal; Missing `}`.", {
2✔
188
            line,
189
            column,
190
          });
191
        } else if (this.peekRead("}")) {
12✔
192
          break;
2✔
193
        } else {
194
          const key = (this.parseStringLiteral().value as StringValue).value;
10✔
195

196
          this.skipWhitespace();
10✔
197
          if (!this.peekRead(":")) {
10✔
198
            throw new SyntaxError("Missing `:' after key.", {
2✔
199
              line: this.line,
200
              column: this.column,
201
            });
202
          }
203
          this.skipWhitespace();
8✔
204

205
          const value = this.parseExpression();
8✔
206

207
          elements.set(key, value);
8✔
208
          this.skipWhitespace();
8✔
209
          if (this.peekRead(",")) {
8✔
210
            continue;
4✔
211
          } else if (this.peekRead("}")) {
4✔
212
            break;
2✔
213
          }
214

215
          throw new SyntaxError("Unterminated record literal; Missing `}'.", {
2✔
216
            line,
217
            column,
218
          });
219
        }
220
      }
221
    }
222

223
    return {
10✔
224
      type: "RecordLiteral",
225
      position: { line, column },
226
      elements,
227
    };
228
  }
229

230
  private parseEscapeSequence(): string {
231
    let c: string;
232

233
    if (this.eof()) {
30✔
234
      throw new SyntaxError("Unterminated escape sequence.", {
2✔
235
        line: this.line,
236
        column: this.column,
237
      });
238
    }
239
    switch ((c = this.read())) {
28✔
240
      case "b":
241
        return "\b";
2✔
242

243
      case "t":
244
        return "\t";
2✔
245

246
      case "n":
247
        return "\n";
2✔
248

249
      case "f":
250
        return "\f";
2✔
251

252
      case "r":
253
        return "\r";
2✔
254

255
      case '"':
256
      case "'":
257
      case "\\":
258
      case "/":
259
        return c;
8✔
260

261
      case "u": {
262
        let result = 0;
8✔
263

264
        for (let i = 0; i < 4; ++i) {
8✔
265
          if (this.eof()) {
26✔
266
            throw new SyntaxError("Unterminated escape sequence.", {
2✔
267
              line: this.line,
268
              column: this.column,
269
            });
270
          } else if (!this.peek(/[a-fA-F0-9]/)) {
24✔
271
            throw new SyntaxError("Illegal Unicode hex escape sequence.", {
2✔
272
              line: this.line,
273
              column: this.column,
274
            });
275
          }
276
          if (this.peek(/[A-F]/)) {
22✔
277
            result =
2✔
278
              result * 16 +
279
              ((this.read().codePointAt(0) ?? 0) -
6!
280
                ("A".codePointAt(0) ?? 0) +
6!
281
                10);
282
          } else if (this.peek(/[a-f]/)) {
20✔
283
            result =
4✔
284
              result * 16 +
285
              ((this.read().codePointAt(0) ?? 0) -
12!
286
                ("a".codePointAt(0) ?? 0) +
12!
287
                10);
288
          } else {
289
            result =
16✔
290
              result * 16 +
291
              ((this.read().codePointAt(0) ?? 0) - ("0".codePointAt(0) ?? 0));
96!
292
          }
293
        }
294

295
        return String.fromCodePoint(result);
4✔
296
      }
297

298
      default:
299
        throw new SyntaxError("Unrecognized escape sequence.", {
2✔
300
          line: this.line,
301
          column: this.column,
302
        });
303
    }
304
  }
305

306
  private parseStringLiteral(): LiteralNode {
307
    let buffer = "";
52✔
308
    let separator: '"' | "'";
309

310
    this.skipWhitespace();
52✔
311

312
    const line = this.line;
52✔
313
    const column = this.column;
52✔
314

315
    if (this.peekRead('"')) {
52✔
316
      separator = '"';
36✔
317
    } else if (this.peekRead("'")) {
16!
318
      separator = "'";
16✔
319
    } else {
320
      throw new SyntaxError(
×
321
        `Unexpected ${
322
          this.eof() ? "end of input" : "input"
×
323
        }; Missing string literal.`,
324
        { line, column },
325
      );
326
    }
327

328
    for (;;) {
52✔
329
      if (this.eof()) {
158✔
330
        throw new SyntaxError(
4✔
331
          `Unterminated string literal; Missing \`${separator}'.`,
332
          { line, column },
333
        );
334
      } else if (this.peekRead(separator)) {
154✔
335
        break;
40✔
336
      } else if (this.peekRead("\\")) {
114✔
337
        buffer += this.parseEscapeSequence();
30✔
338
      } else {
339
        buffer += this.read();
84✔
340
      }
341
    }
342

343
    return {
40✔
344
      type: "Literal",
345
      position: { line, column },
346
      value: newStringValue(buffer),
347
    };
348
  }
349

350
  private parseSymbol(): SymbolNode {
351
    let buffer = "";
20✔
352

353
    this.skipWhitespace();
20✔
354

355
    const line = this.line;
20✔
356
    const column = this.column;
20✔
357

358
    if (!this.peek(isSymbol)) {
20!
359
      throw new SyntaxError(
×
360
        `Unexpected ${this.eof() ? "end of input" : "input"}; Missing symbol.`,
×
361
        { line, column },
362
      );
363
    }
364

365
    do {
20✔
366
      buffer += this.read();
26✔
367
    } while (this.peek(isSymbol));
368

369
    return {
20✔
370
      type: "Symbol",
371
      position: { line, column },
372
      id: buffer,
373
    };
374
  }
375

376
  private parseStatementSymbol(): DefinitionNode | SymbolNode {
377
    let buffer = "";
26✔
378

379
    this.skipWhitespace();
26✔
380

381
    const line = this.line;
26✔
382
    const column = this.column;
26✔
383

384
    if (!this.peek(isSymbol)) {
26✔
385
      throw new SyntaxError(
2✔
386
        `Unexpected ${
387
          this.eof() ? "end of input" : "input"
2!
388
        }; Missing symbol or definition.`,
389
        { line, column },
390
      );
391
    }
392

393
    do {
24✔
394
      buffer += this.read();
68✔
395
    } while (this.peek(isSymbol));
396

397
    if (buffer === "->") {
24✔
398
      const symbol = this.parseSymbol();
2✔
399

400
      return {
2✔
401
        type: "Definition",
402
        position: { line, column },
403
        id: symbol.id,
404
      } as DefinitionNode;
405
    }
406

407
    return {
22✔
408
      type: "Symbol",
409
      position: { line, column },
410
      id: buffer,
411
    } as SymbolNode;
412
  }
413

414
  /**
415
   * Returns true if there are no more characters to be read from the source
416
   * code.
417
   */
418
  private eof(): boolean {
419
    return this.offset >= this.source.length;
1,038✔
420
  }
421

422
  /**
423
   * Advances to next character in the source code and returns the current one.
424
   */
425
  private read(): string {
426
    const result = this.source[this.offset++];
532✔
427

428
    if (result === "\n") {
532✔
429
      ++this.line;
4✔
430
      this.column = 1;
4✔
431
    } else {
432
      ++this.column;
528✔
433
    }
434

435
    return result;
532✔
436
  }
437

438
  /**
439
   * Returns true if next character from the source code matches with given
440
   * pattern.
441
   */
442
  private peek(pattern: RegExp): boolean {
443
    return pattern.test(this.source[this.offset]);
700✔
444
  }
445

446
  /**
447
   * Returns true and advances to next character in the source code if current
448
   * one equals with the one given as argument.
449
   */
450
  private peekRead(expected: string): boolean {
451
    if (this.source[this.offset] === expected) {
1,016✔
452
      this.read();
170✔
453

454
      return true;
170✔
455
    }
456

457
    return false;
846✔
458
  }
459

460
  /**
461
   * Skips whitespace and comments from the source code.
462
   */
463
  private skipWhitespace(): void {
464
    while (!this.eof()) {
506✔
465
      // Skip line comments.
466
      if (this.peekRead("#")) {
496✔
467
        while (!this.eof()) {
2✔
468
          if (this.peekRead("\n") || this.peekRead("\r")) {
38✔
469
            break;
2✔
470
          } else {
471
            this.read();
36✔
472
          }
473
        }
474
      } else if (!this.peek(isSpace)) {
494✔
475
        return;
436✔
476
      } else {
477
        this.read();
58✔
478
      }
479
    }
480
  }
481
}
482

483
export const parse = (
10✔
484
  sourceCode: string,
485
  line: number = 1,
80✔
486
  column: number = 1,
80✔
487
): Node[] => new Parser(sourceCode, line, column).parseScript();
86✔
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