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.
- package/dist/__tests__/checker.test.js +9 -6
- package/dist/__tests__/checker.test.js.map +1 -1
- package/dist/checker/checker.d.ts +2 -0
- package/dist/checker/checker.d.ts.map +1 -1
- package/dist/checker/checker.js +125 -7
- package/dist/checker/checker.js.map +1 -1
- package/dist/compiler/compiler.d.ts.map +1 -1
- package/dist/compiler/compiler.js +45 -14
- package/dist/compiler/compiler.js.map +1 -1
- package/dist/formatter/formatter.d.ts +1 -0
- package/dist/formatter/formatter.d.ts.map +1 -1
- package/dist/formatter/formatter.js +78 -8
- package/dist/formatter/formatter.js.map +1 -1
- package/dist/interpreter/interpreter.d.ts +2 -0
- package/dist/interpreter/interpreter.d.ts.map +1 -1
- package/dist/interpreter/interpreter.js +158 -16
- package/dist/interpreter/interpreter.js.map +1 -1
- package/dist/interpreter/runtime.d.ts +2 -1
- package/dist/interpreter/runtime.d.ts.map +1 -1
- package/dist/interpreter/runtime.js +6 -2
- package/dist/interpreter/runtime.js.map +1 -1
- package/dist/lexer/lexer.d.ts +4 -0
- package/dist/lexer/lexer.d.ts.map +1 -1
- package/dist/lexer/lexer.js +215 -14
- package/dist/lexer/lexer.js.map +1 -1
- package/dist/parser/parser.d.ts +19 -0
- package/dist/parser/parser.d.ts.map +1 -1
- package/dist/parser/parser.js +389 -26
- package/dist/parser/parser.js.map +1 -1
- package/dist/types/ast.d.ts +34 -3
- package/dist/types/ast.d.ts.map +1 -1
- package/dist/types/token.d.ts +25 -0
- package/dist/types/token.d.ts.map +1 -1
- package/dist/types/token.js +33 -0
- package/dist/types/token.js.map +1 -1
- package/package.json +1 -1
package/dist/parser/parser.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
170
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
568
|
+
let left = this.parseNullish();
|
|
403
569
|
while (this.match(TokenType.OR)) {
|
|
404
|
-
const right = this.
|
|
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.
|
|
596
|
+
let left = this.parseBitOr();
|
|
417
597
|
while (this.match(TokenType.AND)) {
|
|
418
|
-
const right = this.
|
|
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.
|
|
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] === '#'
|
|
729
|
-
accidental =
|
|
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
|
|
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;
|