takomusic 1.2.0 → 1.3.1
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/cli/commands/build.js +37 -7
- package/dist/cli/commands/build.js.map +1 -1
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/import.js +7 -0
- package/dist/cli/commands/import.js.map +1 -1
- package/dist/cli/commands/render.d.ts.map +1 -1
- package/dist/cli/commands/render.js +34 -2
- package/dist/cli/commands/render.js.map +1 -1
- package/dist/compiler/compiler.d.ts.map +1 -1
- package/dist/compiler/compiler.js +87 -15
- package/dist/compiler/compiler.js.map +1 -1
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +7 -1
- package/dist/config/config.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/generators/midi.js +2 -1
- package/dist/generators/midi.js.map +1 -1
- package/dist/generators/musicxml.d.ts.map +1 -1
- package/dist/generators/musicxml.js +36 -24
- package/dist/generators/musicxml.js.map +1 -1
- package/dist/importers/musicxml.d.ts.map +1 -1
- package/dist/importers/musicxml.js +26 -10
- package/dist/importers/musicxml.js.map +1 -1
- package/dist/interpreter/builtins/midi.d.ts.map +1 -1
- package/dist/interpreter/builtins/midi.js +23 -0
- package/dist/interpreter/builtins/midi.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 +199 -29
- 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/interpreter/scope.d.ts.map +1 -1
- package/dist/interpreter/scope.js +5 -4
- package/dist/interpreter/scope.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 +437 -30
- 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,11 +737,25 @@ 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() {
|
|
506
753
|
let expr = this.parsePrimary();
|
|
507
|
-
|
|
754
|
+
// Safety limit to prevent infinite loops in pathological cases
|
|
755
|
+
const MAX_CHAIN_LENGTH = 1000;
|
|
756
|
+
let chainLength = 0;
|
|
757
|
+
while (chainLength < MAX_CHAIN_LENGTH) {
|
|
758
|
+
chainLength++;
|
|
508
759
|
if (this.check(TokenType.LPAREN)) {
|
|
509
760
|
// Function call: expr(args) - supports first-class functions
|
|
510
761
|
expr = this.finishCall(expr);
|
|
@@ -518,6 +769,7 @@ export class Parser {
|
|
|
518
769
|
kind: 'IndexExpression',
|
|
519
770
|
object: expr,
|
|
520
771
|
index,
|
|
772
|
+
optional: false,
|
|
521
773
|
position: pos,
|
|
522
774
|
};
|
|
523
775
|
}
|
|
@@ -529,13 +781,45 @@ export class Parser {
|
|
|
529
781
|
kind: 'MemberExpression',
|
|
530
782
|
object: expr,
|
|
531
783
|
property: prop.value,
|
|
784
|
+
optional: false,
|
|
532
785
|
position: pos,
|
|
533
786
|
};
|
|
534
787
|
}
|
|
788
|
+
else if (this.check(TokenType.QUESTIONDOT)) {
|
|
789
|
+
// Optional chaining: expr?.property or expr?.[index]
|
|
790
|
+
const pos = this.advance().position; // consume '?.'
|
|
791
|
+
if (this.check(TokenType.LBRACKET)) {
|
|
792
|
+
// Optional index: expr?.[index]
|
|
793
|
+
this.advance(); // consume '['
|
|
794
|
+
const index = this.parseExpression();
|
|
795
|
+
this.expect(TokenType.RBRACKET, "Expected ']' after index");
|
|
796
|
+
expr = {
|
|
797
|
+
kind: 'IndexExpression',
|
|
798
|
+
object: expr,
|
|
799
|
+
index,
|
|
800
|
+
optional: true,
|
|
801
|
+
position: pos,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
else {
|
|
805
|
+
// Optional property: expr?.property
|
|
806
|
+
const prop = this.expect(TokenType.IDENT, 'Expected property name after "?."');
|
|
807
|
+
expr = {
|
|
808
|
+
kind: 'MemberExpression',
|
|
809
|
+
object: expr,
|
|
810
|
+
property: prop.value,
|
|
811
|
+
optional: true,
|
|
812
|
+
position: pos,
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
}
|
|
535
816
|
else {
|
|
536
817
|
break;
|
|
537
818
|
}
|
|
538
819
|
}
|
|
820
|
+
if (chainLength >= MAX_CHAIN_LENGTH) {
|
|
821
|
+
throw this.error('Expression chain too long (possible infinite loop in parser)');
|
|
822
|
+
}
|
|
539
823
|
return expr;
|
|
540
824
|
}
|
|
541
825
|
finishCall(callee) {
|
|
@@ -611,6 +895,14 @@ export class Parser {
|
|
|
611
895
|
position: token.position,
|
|
612
896
|
};
|
|
613
897
|
}
|
|
898
|
+
// Null literal
|
|
899
|
+
if (this.check(TokenType.NULL)) {
|
|
900
|
+
this.advance();
|
|
901
|
+
return {
|
|
902
|
+
kind: 'NullLiteral',
|
|
903
|
+
position: token.position,
|
|
904
|
+
};
|
|
905
|
+
}
|
|
614
906
|
// Pitch literal
|
|
615
907
|
if (this.check(TokenType.PITCH)) {
|
|
616
908
|
this.advance();
|
|
@@ -634,6 +926,10 @@ export class Parser {
|
|
|
634
926
|
if (this.check(TokenType.LBRACE)) {
|
|
635
927
|
return this.parseObjectLiteral();
|
|
636
928
|
}
|
|
929
|
+
// Template literal
|
|
930
|
+
if (this.check(TokenType.TEMPLATE_STRING) || this.check(TokenType.TEMPLATE_HEAD)) {
|
|
931
|
+
return this.parseTemplateLiteral();
|
|
932
|
+
}
|
|
637
933
|
// Identifier
|
|
638
934
|
if (this.check(TokenType.IDENT)) {
|
|
639
935
|
this.advance();
|
|
@@ -720,13 +1016,33 @@ export class Parser {
|
|
|
720
1016
|
};
|
|
721
1017
|
}
|
|
722
1018
|
parsePitchLiteral(token) {
|
|
723
|
-
// Parse pitch like C4, C#4, Db4
|
|
1019
|
+
// Parse pitch like C4, C#4, Db4, C##4, Cx4, Cbb4
|
|
724
1020
|
const value = token.value;
|
|
725
1021
|
let idx = 0;
|
|
726
1022
|
const noteChar = value[idx++].toUpperCase();
|
|
727
1023
|
let accidental = '';
|
|
728
|
-
if (value[idx] === '#'
|
|
729
|
-
accidental =
|
|
1024
|
+
if (value[idx] === '#') {
|
|
1025
|
+
accidental = '#';
|
|
1026
|
+
idx++;
|
|
1027
|
+
// Check for double sharp (##)
|
|
1028
|
+
if (value[idx] === '#') {
|
|
1029
|
+
accidental = '##';
|
|
1030
|
+
idx++;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
else if (value[idx] === 'x') {
|
|
1034
|
+
// 'x' notation for double sharp
|
|
1035
|
+
accidental = '##';
|
|
1036
|
+
idx++;
|
|
1037
|
+
}
|
|
1038
|
+
else if (value[idx] === 'b') {
|
|
1039
|
+
accidental = 'b';
|
|
1040
|
+
idx++;
|
|
1041
|
+
// Check for double flat (bb)
|
|
1042
|
+
if (value[idx] === 'b') {
|
|
1043
|
+
accidental = 'bb';
|
|
1044
|
+
idx++;
|
|
1045
|
+
}
|
|
730
1046
|
}
|
|
731
1047
|
const octaveStr = value.slice(idx);
|
|
732
1048
|
const octave = parseInt(octaveStr, 10);
|
|
@@ -741,28 +1057,69 @@ export class Parser {
|
|
|
741
1057
|
};
|
|
742
1058
|
}
|
|
743
1059
|
noteToMidi(note, octave) {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
'
|
|
748
|
-
'
|
|
749
|
-
'
|
|
750
|
-
'
|
|
751
|
-
'
|
|
1060
|
+
// Note offsets and octave adjustments for enharmonic equivalents
|
|
1061
|
+
// B# = C of next octave, Cb = B of previous octave, etc.
|
|
1062
|
+
const noteData = {
|
|
1063
|
+
'C': { offset: 0, octaveAdjust: 0 },
|
|
1064
|
+
'C#': { offset: 1, octaveAdjust: 0 },
|
|
1065
|
+
'Db': { offset: 1, octaveAdjust: 0 },
|
|
1066
|
+
'C##': { offset: 2, octaveAdjust: 0 },
|
|
1067
|
+
'Cbb': { offset: 10, octaveAdjust: -1 }, // Cbb = Bb of previous octave
|
|
1068
|
+
'D': { offset: 2, octaveAdjust: 0 },
|
|
1069
|
+
'D#': { offset: 3, octaveAdjust: 0 },
|
|
1070
|
+
'Eb': { offset: 3, octaveAdjust: 0 },
|
|
1071
|
+
'D##': { offset: 4, octaveAdjust: 0 },
|
|
1072
|
+
'Dbb': { offset: 0, octaveAdjust: 0 },
|
|
1073
|
+
'E': { offset: 4, octaveAdjust: 0 },
|
|
1074
|
+
'Fb': { offset: 4, octaveAdjust: 0 },
|
|
1075
|
+
'E#': { offset: 5, octaveAdjust: 0 },
|
|
1076
|
+
'E##': { offset: 6, octaveAdjust: 0 },
|
|
1077
|
+
'Ebb': { offset: 2, octaveAdjust: 0 },
|
|
1078
|
+
'F': { offset: 5, octaveAdjust: 0 },
|
|
1079
|
+
'F#': { offset: 6, octaveAdjust: 0 },
|
|
1080
|
+
'Gb': { offset: 6, octaveAdjust: 0 },
|
|
1081
|
+
'F##': { offset: 7, octaveAdjust: 0 },
|
|
1082
|
+
'Fbb': { offset: 3, octaveAdjust: 0 },
|
|
1083
|
+
'G': { offset: 7, octaveAdjust: 0 },
|
|
1084
|
+
'G#': { offset: 8, octaveAdjust: 0 },
|
|
1085
|
+
'Ab': { offset: 8, octaveAdjust: 0 },
|
|
1086
|
+
'G##': { offset: 9, octaveAdjust: 0 },
|
|
1087
|
+
'Gbb': { offset: 5, octaveAdjust: 0 },
|
|
1088
|
+
'A': { offset: 9, octaveAdjust: 0 },
|
|
1089
|
+
'A#': { offset: 10, octaveAdjust: 0 },
|
|
1090
|
+
'Bb': { offset: 10, octaveAdjust: 0 },
|
|
1091
|
+
'A##': { offset: 11, octaveAdjust: 0 },
|
|
1092
|
+
'Abb': { offset: 7, octaveAdjust: 0 },
|
|
1093
|
+
'B': { offset: 11, octaveAdjust: 0 },
|
|
1094
|
+
'Cb': { offset: 11, octaveAdjust: -1 }, // Cb = B of previous octave
|
|
1095
|
+
'B#': { offset: 0, octaveAdjust: 1 }, // B# = C of next octave
|
|
1096
|
+
'B##': { offset: 1, octaveAdjust: 1 }, // B## = C# of next octave
|
|
1097
|
+
'Bbb': { offset: 9, octaveAdjust: 0 },
|
|
752
1098
|
};
|
|
753
|
-
const
|
|
1099
|
+
const data = noteData[note] ?? { offset: 0, octaveAdjust: 0 };
|
|
754
1100
|
// MIDI: C4 = 60, so C0 = 12
|
|
755
|
-
return (octave + 1) * 12 + offset;
|
|
1101
|
+
return (octave + 1 + data.octaveAdjust) * 12 + data.offset;
|
|
756
1102
|
}
|
|
757
1103
|
parseDurLiteral(token) {
|
|
758
|
-
// Parse duration like 1/4, 3/8
|
|
759
|
-
const
|
|
1104
|
+
// Parse duration like 1/4, 3/8, 1/4., 1/4..
|
|
1105
|
+
const value = token.value;
|
|
1106
|
+
// Count trailing dots
|
|
1107
|
+
let dots = 0;
|
|
1108
|
+
let i = value.length - 1;
|
|
1109
|
+
while (i >= 0 && value[i] === '.') {
|
|
1110
|
+
dots++;
|
|
1111
|
+
i--;
|
|
1112
|
+
}
|
|
1113
|
+
// Extract fraction part (without dots)
|
|
1114
|
+
const fractionPart = value.slice(0, value.length - dots);
|
|
1115
|
+
const [numStr, denStr] = fractionPart.split('/');
|
|
760
1116
|
const numerator = parseInt(numStr, 10);
|
|
761
1117
|
const denominator = parseInt(denStr, 10);
|
|
762
1118
|
return {
|
|
763
1119
|
kind: 'DurLiteral',
|
|
764
1120
|
numerator,
|
|
765
1121
|
denominator,
|
|
1122
|
+
dots,
|
|
766
1123
|
position: token.position,
|
|
767
1124
|
};
|
|
768
1125
|
}
|
|
@@ -844,6 +1201,53 @@ export class Parser {
|
|
|
844
1201
|
position: pos,
|
|
845
1202
|
};
|
|
846
1203
|
}
|
|
1204
|
+
parseTemplateLiteral() {
|
|
1205
|
+
const pos = this.peek().position;
|
|
1206
|
+
const quasis = [];
|
|
1207
|
+
const expressions = [];
|
|
1208
|
+
// Simple template string without interpolation
|
|
1209
|
+
if (this.check(TokenType.TEMPLATE_STRING)) {
|
|
1210
|
+
quasis.push(this.advance().value);
|
|
1211
|
+
return {
|
|
1212
|
+
kind: 'TemplateLiteral',
|
|
1213
|
+
quasis,
|
|
1214
|
+
expressions,
|
|
1215
|
+
position: pos,
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
// Template with interpolation
|
|
1219
|
+
// Head: `text${
|
|
1220
|
+
quasis.push(this.advance().value);
|
|
1221
|
+
// Parse expressions and middle/tail parts
|
|
1222
|
+
// Safety limit to prevent infinite loops
|
|
1223
|
+
const MAX_TEMPLATE_PARTS = 1000;
|
|
1224
|
+
let partCount = 0;
|
|
1225
|
+
while (partCount < MAX_TEMPLATE_PARTS) {
|
|
1226
|
+
partCount++;
|
|
1227
|
+
// Parse expression between ${ and }
|
|
1228
|
+
expressions.push(this.parseExpression());
|
|
1229
|
+
// Expect TEMPLATE_MIDDLE or TEMPLATE_TAIL
|
|
1230
|
+
if (this.check(TokenType.TEMPLATE_MIDDLE)) {
|
|
1231
|
+
quasis.push(this.advance().value);
|
|
1232
|
+
}
|
|
1233
|
+
else if (this.check(TokenType.TEMPLATE_TAIL)) {
|
|
1234
|
+
quasis.push(this.advance().value);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
else {
|
|
1238
|
+
throw this.error('Expected template continuation');
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (partCount >= MAX_TEMPLATE_PARTS) {
|
|
1242
|
+
throw this.error('Template literal has too many parts (possible infinite loop)');
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
kind: 'TemplateLiteral',
|
|
1246
|
+
quasis,
|
|
1247
|
+
expressions,
|
|
1248
|
+
position: pos,
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
847
1251
|
// Helper methods
|
|
848
1252
|
peek() {
|
|
849
1253
|
return this.tokens[this.current];
|
|
@@ -861,6 +1265,9 @@ export class Parser {
|
|
|
861
1265
|
}
|
|
862
1266
|
return this.tokens[this.current - 1];
|
|
863
1267
|
}
|
|
1268
|
+
previous() {
|
|
1269
|
+
return this.tokens[this.current - 1];
|
|
1270
|
+
}
|
|
864
1271
|
check(type) {
|
|
865
1272
|
if (this.isAtEnd())
|
|
866
1273
|
return false;
|