takomusic 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/dist/__tests__/checker.test.js +9 -6
  2. package/dist/__tests__/checker.test.js.map +1 -1
  3. package/dist/checker/checker.d.ts +2 -0
  4. package/dist/checker/checker.d.ts.map +1 -1
  5. package/dist/checker/checker.js +125 -7
  6. package/dist/checker/checker.js.map +1 -1
  7. package/dist/compiler/compiler.d.ts.map +1 -1
  8. package/dist/compiler/compiler.js +45 -14
  9. package/dist/compiler/compiler.js.map +1 -1
  10. package/dist/formatter/formatter.d.ts +1 -0
  11. package/dist/formatter/formatter.d.ts.map +1 -1
  12. package/dist/formatter/formatter.js +78 -8
  13. package/dist/formatter/formatter.js.map +1 -1
  14. package/dist/interpreter/interpreter.d.ts +2 -0
  15. package/dist/interpreter/interpreter.d.ts.map +1 -1
  16. package/dist/interpreter/interpreter.js +158 -16
  17. package/dist/interpreter/interpreter.js.map +1 -1
  18. package/dist/interpreter/runtime.d.ts +2 -1
  19. package/dist/interpreter/runtime.d.ts.map +1 -1
  20. package/dist/interpreter/runtime.js +6 -2
  21. package/dist/interpreter/runtime.js.map +1 -1
  22. package/dist/lexer/lexer.d.ts +4 -0
  23. package/dist/lexer/lexer.d.ts.map +1 -1
  24. package/dist/lexer/lexer.js +215 -14
  25. package/dist/lexer/lexer.js.map +1 -1
  26. package/dist/parser/parser.d.ts +19 -0
  27. package/dist/parser/parser.d.ts.map +1 -1
  28. package/dist/parser/parser.js +389 -26
  29. package/dist/parser/parser.js.map +1 -1
  30. package/dist/types/ast.d.ts +34 -3
  31. package/dist/types/ast.d.ts.map +1 -1
  32. package/dist/types/token.d.ts +25 -0
  33. package/dist/types/token.d.ts.map +1 -1
  34. package/dist/types/token.js +33 -0
  35. package/dist/types/token.js.map +1 -1
  36. package/package.json +1 -1
@@ -5,6 +5,8 @@ export class Parser {
5
5
  tokens;
6
6
  current = 0;
7
7
  filePath;
8
+ errors = [];
9
+ panicMode = false;
8
10
  constructor(tokens, filePath) {
9
11
  this.tokens = tokens;
10
12
  this.filePath = filePath;
@@ -20,6 +22,66 @@ export class Parser {
20
22
  }
21
23
  return { kind: 'Program', statements, position: pos };
22
24
  }
25
+ // Parse with error recovery - collects multiple errors instead of stopping at first
26
+ parseWithErrors() {
27
+ this.errors = [];
28
+ const statements = [];
29
+ const pos = this.peek().position;
30
+ while (!this.isAtEnd()) {
31
+ try {
32
+ this.panicMode = false;
33
+ const stmt = this.parseStatement();
34
+ if (stmt) {
35
+ statements.push(stmt);
36
+ }
37
+ }
38
+ catch (e) {
39
+ if (e instanceof MFError) {
40
+ this.errors.push(e);
41
+ this.synchronize();
42
+ }
43
+ else {
44
+ throw e;
45
+ }
46
+ }
47
+ }
48
+ const program = { kind: 'Program', statements, position: pos };
49
+ return { program, errors: this.errors };
50
+ }
51
+ // Synchronize after an error by skipping to the next statement boundary
52
+ synchronize() {
53
+ this.panicMode = true;
54
+ this.advance();
55
+ while (!this.isAtEnd()) {
56
+ // If we just passed a semicolon, we're at a statement boundary
57
+ if (this.previous().type === TokenType.SEMICOLON) {
58
+ return;
59
+ }
60
+ // If the current token starts a new statement, we're synchronized
61
+ switch (this.peek().type) {
62
+ case TokenType.PROC:
63
+ case TokenType.CONST:
64
+ case TokenType.LET:
65
+ case TokenType.IF:
66
+ case TokenType.FOR:
67
+ case TokenType.WHILE:
68
+ case TokenType.MATCH:
69
+ case TokenType.RETURN:
70
+ case TokenType.BREAK:
71
+ case TokenType.CONTINUE:
72
+ case TokenType.IMPORT:
73
+ case TokenType.EXPORT:
74
+ case TokenType.VOCAL:
75
+ case TokenType.MIDI:
76
+ return;
77
+ }
78
+ this.advance();
79
+ }
80
+ }
81
+ // Get collected errors (for parseWithErrors)
82
+ getErrors() {
83
+ return this.errors;
84
+ }
23
85
  parseStatement() {
24
86
  if (this.check(TokenType.IMPORT)) {
25
87
  return this.parseImport();
@@ -45,6 +107,9 @@ export class Parser {
45
107
  if (this.check(TokenType.WHILE)) {
46
108
  return this.parseWhile();
47
109
  }
110
+ if (this.check(TokenType.MATCH)) {
111
+ return this.parseMatch();
112
+ }
48
113
  if (this.check(TokenType.RETURN)) {
49
114
  return this.parseReturn();
50
115
  }
@@ -63,7 +128,24 @@ export class Parser {
63
128
  }
64
129
  parseImport() {
65
130
  const pos = this.advance().position; // consume 'import'
66
- this.expect(TokenType.LBRACE, "Expected '{' after 'import'");
131
+ // Check for wildcard import: import * as name from "path"
132
+ if (this.check(TokenType.STAR)) {
133
+ this.advance(); // consume '*'
134
+ this.expect(TokenType.AS, "Expected 'as' after '*' in import");
135
+ const nameToken = this.expect(TokenType.IDENT, 'Expected namespace identifier after "as"');
136
+ this.expect(TokenType.IDENT, "Expected 'from'"); // 'from' keyword
137
+ const pathToken = this.expect(TokenType.STRING, 'Expected module path');
138
+ this.expect(TokenType.SEMICOLON, "Expected ';' after import");
139
+ return {
140
+ kind: 'ImportStatement',
141
+ imports: [],
142
+ namespace: nameToken.value,
143
+ path: pathToken.value,
144
+ position: pos,
145
+ };
146
+ }
147
+ // Named imports: import { a, b } from "path"
148
+ this.expect(TokenType.LBRACE, "Expected '{' or '*' after 'import'");
67
149
  const imports = [];
68
150
  if (!this.check(TokenType.RBRACE)) {
69
151
  do {
@@ -106,6 +188,7 @@ export class Parser {
106
188
  this.expect(TokenType.LPAREN, "Expected '(' after proc name");
107
189
  const params = [];
108
190
  if (!this.check(TokenType.RPAREN)) {
191
+ let hasDefault = false;
109
192
  do {
110
193
  // Check for rest parameter
111
194
  if (this.check(TokenType.SPREAD)) {
@@ -115,7 +198,18 @@ export class Parser {
115
198
  break; // Rest must be last
116
199
  }
117
200
  const param = this.expect(TokenType.IDENT, 'Expected parameter name');
118
- params.push({ name: param.value });
201
+ // Check for default value
202
+ if (this.match(TokenType.EQ)) {
203
+ hasDefault = true;
204
+ const defaultValue = this.parseExpression();
205
+ params.push({ name: param.value, defaultValue });
206
+ }
207
+ else {
208
+ if (hasDefault) {
209
+ throw this.error('Required parameters cannot follow parameters with default values');
210
+ }
211
+ params.push({ name: param.value });
212
+ }
119
213
  } while (this.match(TokenType.COMMA));
120
214
  }
121
215
  this.expect(TokenType.RPAREN, "Expected ')' after parameters");
@@ -166,8 +260,14 @@ export class Parser {
166
260
  const consequent = this.parseBlock();
167
261
  let alternate = null;
168
262
  if (this.match(TokenType.ELSE)) {
169
- this.expect(TokenType.LBRACE, "Expected '{' after 'else'");
170
- alternate = this.parseBlock();
263
+ if (this.check(TokenType.IF)) {
264
+ // else if: recursively parse as nested IfStatement
265
+ alternate = this.parseIf();
266
+ }
267
+ else {
268
+ this.expect(TokenType.LBRACE, "Expected '{' after 'else'");
269
+ alternate = this.parseBlock();
270
+ }
171
271
  }
172
272
  return {
173
273
  kind: 'IfStatement',
@@ -240,6 +340,44 @@ export class Parser {
240
340
  position: pos,
241
341
  };
242
342
  }
343
+ parseMatch() {
344
+ const pos = this.advance().position; // consume 'match'
345
+ this.expect(TokenType.LPAREN, "Expected '(' after 'match'");
346
+ const expression = this.parseExpression();
347
+ this.expect(TokenType.RPAREN, "Expected ')' after match expression");
348
+ this.expect(TokenType.LBRACE, "Expected '{' before match cases");
349
+ const cases = [];
350
+ let hasDefault = false;
351
+ while (!this.check(TokenType.RBRACE) && !this.isAtEnd()) {
352
+ if (this.check(TokenType.CASE)) {
353
+ this.advance(); // consume 'case'
354
+ const pattern = this.parseExpression();
355
+ this.expect(TokenType.LBRACE, "Expected '{' before case body");
356
+ const body = this.parseBlock();
357
+ cases.push({ pattern, body });
358
+ }
359
+ else if (this.check(TokenType.DEFAULT)) {
360
+ if (hasDefault) {
361
+ throw new MFError('SYNTAX', 'Multiple default cases in match statement', this.peek().position, this.filePath);
362
+ }
363
+ this.advance(); // consume 'default'
364
+ this.expect(TokenType.LBRACE, "Expected '{' before default body");
365
+ const body = this.parseBlock();
366
+ cases.push({ pattern: null, body });
367
+ hasDefault = true;
368
+ }
369
+ else {
370
+ throw new MFError('SYNTAX', "Expected 'case' or 'default' in match statement", this.peek().position, this.filePath);
371
+ }
372
+ }
373
+ this.expect(TokenType.RBRACE, "Expected '}' after match cases");
374
+ return {
375
+ kind: 'MatchStatement',
376
+ expression,
377
+ cases,
378
+ position: pos,
379
+ };
380
+ }
243
381
  parseReturn() {
244
382
  const pos = this.advance().position; // consume 'return'
245
383
  let value = null;
@@ -341,10 +479,10 @@ export class Parser {
341
479
  }
342
480
  throw this.error('Invalid assignment target');
343
481
  }
344
- // Compound assignment: +=, -=, *=, /=
482
+ // Compound assignment: +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=
345
483
  if (this.checkCompoundAssign()) {
346
484
  const op = this.advance().value; // e.g., '+='
347
- const binaryOp = op.charAt(0); // e.g., '+'
485
+ const binaryOp = this.getCompoundAssignOp(op); // e.g., '+'
348
486
  const rhs = this.parseExpression();
349
487
  this.expect(TokenType.SEMICOLON, "Expected ';' after assignment");
350
488
  // Desugar: x += y -> x = x + y
@@ -392,16 +530,44 @@ export class Parser {
392
530
  }
393
531
  checkCompoundAssign() {
394
532
  return this.check(TokenType.PLUSEQ) || this.check(TokenType.MINUSEQ) ||
395
- this.check(TokenType.STAREQ) || this.check(TokenType.SLASHEQ);
533
+ this.check(TokenType.STAREQ) || this.check(TokenType.SLASHEQ) ||
534
+ this.check(TokenType.PERCENTEQ) || this.check(TokenType.BITANDEQ) ||
535
+ this.check(TokenType.BITOREQ) || this.check(TokenType.BITXOREQ) ||
536
+ this.check(TokenType.SHLEQ) || this.check(TokenType.SHREQ);
537
+ }
538
+ getCompoundAssignOp(op) {
539
+ // Map compound assignment operator to its binary operator
540
+ const opMap = {
541
+ '+=': '+', '-=': '-', '*=': '*', '/=': '/', '%=': '%',
542
+ '&=': '&', '|=': '|', '^=': '^', '<<=': '<<', '>>=': '>>'
543
+ };
544
+ return opMap[op] || op.charAt(0);
396
545
  }
397
546
  // Expression parsing with precedence climbing
398
547
  parseExpression() {
399
- return this.parseOr();
548
+ return this.parseConditional();
549
+ }
550
+ // Ternary operator: a ? b : c (lowest precedence)
551
+ parseConditional() {
552
+ let expr = this.parseOr();
553
+ if (this.match(TokenType.QUESTION)) {
554
+ const consequent = this.parseExpression();
555
+ this.expect(TokenType.COLON, "Expected ':' in ternary expression");
556
+ const alternate = this.parseConditional();
557
+ expr = {
558
+ kind: 'ConditionalExpression',
559
+ condition: expr,
560
+ consequent,
561
+ alternate,
562
+ position: expr.position,
563
+ };
564
+ }
565
+ return expr;
400
566
  }
401
567
  parseOr() {
402
- let left = this.parseAnd();
568
+ let left = this.parseNullish();
403
569
  while (this.match(TokenType.OR)) {
404
- const right = this.parseAnd();
570
+ const right = this.parseNullish();
405
571
  left = {
406
572
  kind: 'BinaryExpression',
407
573
  operator: '||',
@@ -412,10 +578,24 @@ export class Parser {
412
578
  }
413
579
  return left;
414
580
  }
581
+ parseNullish() {
582
+ let left = this.parseAnd();
583
+ while (this.match(TokenType.NULLISH)) {
584
+ const right = this.parseAnd();
585
+ left = {
586
+ kind: 'BinaryExpression',
587
+ operator: '??',
588
+ left,
589
+ right,
590
+ position: left.position,
591
+ };
592
+ }
593
+ return left;
594
+ }
415
595
  parseAnd() {
416
- let left = this.parseEquality();
596
+ let left = this.parseBitOr();
417
597
  while (this.match(TokenType.AND)) {
418
- const right = this.parseEquality();
598
+ const right = this.parseBitOr();
419
599
  left = {
420
600
  kind: 'BinaryExpression',
421
601
  operator: '&&',
@@ -426,6 +606,48 @@ export class Parser {
426
606
  }
427
607
  return left;
428
608
  }
609
+ parseBitOr() {
610
+ let left = this.parseBitXor();
611
+ while (this.match(TokenType.BITOR)) {
612
+ const right = this.parseBitXor();
613
+ left = {
614
+ kind: 'BinaryExpression',
615
+ operator: '|',
616
+ left,
617
+ right,
618
+ position: left.position,
619
+ };
620
+ }
621
+ return left;
622
+ }
623
+ parseBitXor() {
624
+ let left = this.parseBitAnd();
625
+ while (this.match(TokenType.BITXOR)) {
626
+ const right = this.parseBitAnd();
627
+ left = {
628
+ kind: 'BinaryExpression',
629
+ operator: '^',
630
+ left,
631
+ right,
632
+ position: left.position,
633
+ };
634
+ }
635
+ return left;
636
+ }
637
+ parseBitAnd() {
638
+ let left = this.parseEquality();
639
+ while (this.match(TokenType.BITAND)) {
640
+ const right = this.parseEquality();
641
+ left = {
642
+ kind: 'BinaryExpression',
643
+ operator: '&',
644
+ left,
645
+ right,
646
+ position: left.position,
647
+ };
648
+ }
649
+ return left;
650
+ }
429
651
  parseEquality() {
430
652
  let left = this.parseComparison();
431
653
  while (this.check(TokenType.EQEQ) || this.check(TokenType.NEQ)) {
@@ -442,11 +664,26 @@ export class Parser {
442
664
  return left;
443
665
  }
444
666
  parseComparison() {
445
- let left = this.parseAdditive();
667
+ let left = this.parseShift();
446
668
  while (this.check(TokenType.LT) ||
447
669
  this.check(TokenType.GT) ||
448
670
  this.check(TokenType.LTEQ) ||
449
671
  this.check(TokenType.GTEQ)) {
672
+ const op = this.advance().value;
673
+ const right = this.parseShift();
674
+ left = {
675
+ kind: 'BinaryExpression',
676
+ operator: op,
677
+ left,
678
+ right,
679
+ position: left.position,
680
+ };
681
+ }
682
+ return left;
683
+ }
684
+ parseShift() {
685
+ let left = this.parseAdditive();
686
+ while (this.check(TokenType.SHL) || this.check(TokenType.SHR)) {
450
687
  const op = this.advance().value;
451
688
  const right = this.parseAdditive();
452
689
  left = {
@@ -490,7 +727,7 @@ export class Parser {
490
727
  return left;
491
728
  }
492
729
  parseUnary() {
493
- if (this.check(TokenType.NOT) || this.check(TokenType.MINUS)) {
730
+ if (this.check(TokenType.NOT) || this.check(TokenType.MINUS) || this.check(TokenType.BITNOT)) {
494
731
  const op = this.advance().value;
495
732
  const operand = this.parseUnary();
496
733
  return {
@@ -500,6 +737,16 @@ export class Parser {
500
737
  position: operand.position,
501
738
  };
502
739
  }
740
+ // typeof operator
741
+ if (this.check(TokenType.TYPEOF)) {
742
+ const pos = this.advance().position;
743
+ const operand = this.parseUnary();
744
+ return {
745
+ kind: 'TypeofExpression',
746
+ operand,
747
+ position: pos,
748
+ };
749
+ }
503
750
  return this.parseCall();
504
751
  }
505
752
  parseCall() {
@@ -518,6 +765,7 @@ export class Parser {
518
765
  kind: 'IndexExpression',
519
766
  object: expr,
520
767
  index,
768
+ optional: false,
521
769
  position: pos,
522
770
  };
523
771
  }
@@ -529,9 +777,38 @@ export class Parser {
529
777
  kind: 'MemberExpression',
530
778
  object: expr,
531
779
  property: prop.value,
780
+ optional: false,
532
781
  position: pos,
533
782
  };
534
783
  }
784
+ else if (this.check(TokenType.QUESTIONDOT)) {
785
+ // Optional chaining: expr?.property or expr?.[index]
786
+ const pos = this.advance().position; // consume '?.'
787
+ if (this.check(TokenType.LBRACKET)) {
788
+ // Optional index: expr?.[index]
789
+ this.advance(); // consume '['
790
+ const index = this.parseExpression();
791
+ this.expect(TokenType.RBRACKET, "Expected ']' after index");
792
+ expr = {
793
+ kind: 'IndexExpression',
794
+ object: expr,
795
+ index,
796
+ optional: true,
797
+ position: pos,
798
+ };
799
+ }
800
+ else {
801
+ // Optional property: expr?.property
802
+ const prop = this.expect(TokenType.IDENT, 'Expected property name after "?."');
803
+ expr = {
804
+ kind: 'MemberExpression',
805
+ object: expr,
806
+ property: prop.value,
807
+ optional: true,
808
+ position: pos,
809
+ };
810
+ }
811
+ }
535
812
  else {
536
813
  break;
537
814
  }
@@ -611,6 +888,14 @@ export class Parser {
611
888
  position: token.position,
612
889
  };
613
890
  }
891
+ // Null literal
892
+ if (this.check(TokenType.NULL)) {
893
+ this.advance();
894
+ return {
895
+ kind: 'NullLiteral',
896
+ position: token.position,
897
+ };
898
+ }
614
899
  // Pitch literal
615
900
  if (this.check(TokenType.PITCH)) {
616
901
  this.advance();
@@ -634,6 +919,10 @@ export class Parser {
634
919
  if (this.check(TokenType.LBRACE)) {
635
920
  return this.parseObjectLiteral();
636
921
  }
922
+ // Template literal
923
+ if (this.check(TokenType.TEMPLATE_STRING) || this.check(TokenType.TEMPLATE_HEAD)) {
924
+ return this.parseTemplateLiteral();
925
+ }
637
926
  // Identifier
638
927
  if (this.check(TokenType.IDENT)) {
639
928
  this.advance();
@@ -720,13 +1009,33 @@ export class Parser {
720
1009
  };
721
1010
  }
722
1011
  parsePitchLiteral(token) {
723
- // Parse pitch like C4, C#4, Db4
1012
+ // Parse pitch like C4, C#4, Db4, C##4, Cx4, Cbb4
724
1013
  const value = token.value;
725
1014
  let idx = 0;
726
1015
  const noteChar = value[idx++].toUpperCase();
727
1016
  let accidental = '';
728
- if (value[idx] === '#' || value[idx] === 'b') {
729
- accidental = value[idx++];
1017
+ if (value[idx] === '#') {
1018
+ accidental = '#';
1019
+ idx++;
1020
+ // Check for double sharp (##)
1021
+ if (value[idx] === '#') {
1022
+ accidental = '##';
1023
+ idx++;
1024
+ }
1025
+ }
1026
+ else if (value[idx] === 'x') {
1027
+ // 'x' notation for double sharp
1028
+ accidental = '##';
1029
+ idx++;
1030
+ }
1031
+ else if (value[idx] === 'b') {
1032
+ accidental = 'b';
1033
+ idx++;
1034
+ // Check for double flat (bb)
1035
+ if (value[idx] === 'b') {
1036
+ accidental = 'bb';
1037
+ idx++;
1038
+ }
730
1039
  }
731
1040
  const octaveStr = value.slice(idx);
732
1041
  const octave = parseInt(octaveStr, 10);
@@ -742,27 +1051,38 @@ export class Parser {
742
1051
  }
743
1052
  noteToMidi(note, octave) {
744
1053
  const noteOffsets = {
745
- 'C': 0, 'C#': 1, 'Db': 1,
746
- 'D': 2, 'D#': 3, 'Eb': 3,
747
- 'E': 4, 'Fb': 4, 'E#': 5,
748
- 'F': 5, 'F#': 6, 'Gb': 6,
749
- 'G': 7, 'G#': 8, 'Ab': 8,
750
- 'A': 9, 'A#': 10, 'Bb': 10,
751
- 'B': 11, 'Cb': 11, 'B#': 0,
1054
+ 'C': 0, 'C#': 1, 'Db': 1, 'C##': 2, 'Cbb': -2,
1055
+ 'D': 2, 'D#': 3, 'Eb': 3, 'D##': 4, 'Dbb': 0,
1056
+ 'E': 4, 'Fb': 4, 'E#': 5, 'E##': 6, 'Ebb': 2,
1057
+ 'F': 5, 'F#': 6, 'Gb': 6, 'F##': 7, 'Fbb': 3,
1058
+ 'G': 7, 'G#': 8, 'Ab': 8, 'G##': 9, 'Gbb': 5,
1059
+ 'A': 9, 'A#': 10, 'Bb': 10, 'A##': 11, 'Abb': 7,
1060
+ 'B': 11, 'Cb': 11, 'B#': 0, 'B##': 1, 'Bbb': 9,
752
1061
  };
753
1062
  const offset = noteOffsets[note] ?? 0;
754
1063
  // MIDI: C4 = 60, so C0 = 12
755
1064
  return (octave + 1) * 12 + offset;
756
1065
  }
757
1066
  parseDurLiteral(token) {
758
- // Parse duration like 1/4, 3/8
759
- const [numStr, denStr] = token.value.split('/');
1067
+ // Parse duration like 1/4, 3/8, 1/4., 1/4..
1068
+ const value = token.value;
1069
+ // Count trailing dots
1070
+ let dots = 0;
1071
+ let i = value.length - 1;
1072
+ while (i >= 0 && value[i] === '.') {
1073
+ dots++;
1074
+ i--;
1075
+ }
1076
+ // Extract fraction part (without dots)
1077
+ const fractionPart = value.slice(0, value.length - dots);
1078
+ const [numStr, denStr] = fractionPart.split('/');
760
1079
  const numerator = parseInt(numStr, 10);
761
1080
  const denominator = parseInt(denStr, 10);
762
1081
  return {
763
1082
  kind: 'DurLiteral',
764
1083
  numerator,
765
1084
  denominator,
1085
+ dots,
766
1086
  position: token.position,
767
1087
  };
768
1088
  }
@@ -844,6 +1164,46 @@ export class Parser {
844
1164
  position: pos,
845
1165
  };
846
1166
  }
1167
+ parseTemplateLiteral() {
1168
+ const pos = this.peek().position;
1169
+ const quasis = [];
1170
+ const expressions = [];
1171
+ // Simple template string without interpolation
1172
+ if (this.check(TokenType.TEMPLATE_STRING)) {
1173
+ quasis.push(this.advance().value);
1174
+ return {
1175
+ kind: 'TemplateLiteral',
1176
+ quasis,
1177
+ expressions,
1178
+ position: pos,
1179
+ };
1180
+ }
1181
+ // Template with interpolation
1182
+ // Head: `text${
1183
+ quasis.push(this.advance().value);
1184
+ // Parse expressions and middle/tail parts
1185
+ while (true) {
1186
+ // Parse expression between ${ and }
1187
+ expressions.push(this.parseExpression());
1188
+ // Expect TEMPLATE_MIDDLE or TEMPLATE_TAIL
1189
+ if (this.check(TokenType.TEMPLATE_MIDDLE)) {
1190
+ quasis.push(this.advance().value);
1191
+ }
1192
+ else if (this.check(TokenType.TEMPLATE_TAIL)) {
1193
+ quasis.push(this.advance().value);
1194
+ break;
1195
+ }
1196
+ else {
1197
+ throw this.error('Expected template continuation');
1198
+ }
1199
+ }
1200
+ return {
1201
+ kind: 'TemplateLiteral',
1202
+ quasis,
1203
+ expressions,
1204
+ position: pos,
1205
+ };
1206
+ }
847
1207
  // Helper methods
848
1208
  peek() {
849
1209
  return this.tokens[this.current];
@@ -861,6 +1221,9 @@ export class Parser {
861
1221
  }
862
1222
  return this.tokens[this.current - 1];
863
1223
  }
1224
+ previous() {
1225
+ return this.tokens[this.current - 1];
1226
+ }
864
1227
  check(type) {
865
1228
  if (this.isAtEnd())
866
1229
  return false;