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
|
@@ -94,6 +94,10 @@ export class Interpreter {
|
|
|
94
94
|
registerConst(name, value) {
|
|
95
95
|
this.scope.defineConst(name, value);
|
|
96
96
|
}
|
|
97
|
+
// Get the global scope (for namespace imports)
|
|
98
|
+
getGlobalScope() {
|
|
99
|
+
return this.scope;
|
|
100
|
+
}
|
|
97
101
|
execute(program) {
|
|
98
102
|
// First pass: collect all proc declarations from this module
|
|
99
103
|
for (const stmt of program.statements) {
|
|
@@ -156,11 +160,18 @@ export class Interpreter {
|
|
|
156
160
|
this.scope = oldScope;
|
|
157
161
|
}
|
|
158
162
|
else if (stmt.alternate) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
163
|
+
if (Array.isArray(stmt.alternate)) {
|
|
164
|
+
// else: alternate is Statement[]
|
|
165
|
+
const childScope = this.scope.createChild();
|
|
166
|
+
const oldScope = this.scope;
|
|
167
|
+
this.scope = childScope;
|
|
168
|
+
this.executeStatements(stmt.alternate);
|
|
169
|
+
this.scope = oldScope;
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
// else if: alternate is a single IfStatement
|
|
173
|
+
this.executeStatement(stmt.alternate);
|
|
174
|
+
}
|
|
164
175
|
}
|
|
165
176
|
break;
|
|
166
177
|
}
|
|
@@ -229,6 +240,46 @@ export class Interpreter {
|
|
|
229
240
|
}
|
|
230
241
|
break;
|
|
231
242
|
}
|
|
243
|
+
case 'MatchStatement': {
|
|
244
|
+
const matchValue = this.evaluate(stmt.expression);
|
|
245
|
+
let matched = false;
|
|
246
|
+
let defaultCase = null;
|
|
247
|
+
for (const matchCase of stmt.cases) {
|
|
248
|
+
if (matchCase.pattern === null) {
|
|
249
|
+
// Store default case for later
|
|
250
|
+
defaultCase = matchCase;
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const caseValue = this.evaluate(matchCase.pattern);
|
|
254
|
+
if (this.valuesEqual(matchValue, caseValue)) {
|
|
255
|
+
// Execute the matching case
|
|
256
|
+
const childScope = this.scope.createChild();
|
|
257
|
+
const oldScope = this.scope;
|
|
258
|
+
this.scope = childScope;
|
|
259
|
+
try {
|
|
260
|
+
this.executeStatements(matchCase.body);
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
this.scope = oldScope;
|
|
264
|
+
}
|
|
265
|
+
matched = true;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
// If no case matched, execute default case if present
|
|
270
|
+
if (!matched && defaultCase) {
|
|
271
|
+
const childScope = this.scope.createChild();
|
|
272
|
+
const oldScope = this.scope;
|
|
273
|
+
this.scope = childScope;
|
|
274
|
+
try {
|
|
275
|
+
this.executeStatements(defaultCase.body);
|
|
276
|
+
}
|
|
277
|
+
finally {
|
|
278
|
+
this.scope = oldScope;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
232
283
|
case 'ReturnStatement': {
|
|
233
284
|
const value = stmt.value ? this.evaluate(stmt.value) : makeNull();
|
|
234
285
|
throw new ReturnSignal(value);
|
|
@@ -271,10 +322,23 @@ export class Interpreter {
|
|
|
271
322
|
}
|
|
272
323
|
case 'ForEachStatement': {
|
|
273
324
|
const iterable = this.evaluate(stmt.iterable);
|
|
274
|
-
|
|
275
|
-
|
|
325
|
+
// Build the list of elements to iterate over
|
|
326
|
+
let elements = [];
|
|
327
|
+
if (iterable.type === 'array') {
|
|
328
|
+
elements = iterable.elements;
|
|
276
329
|
}
|
|
277
|
-
|
|
330
|
+
else if (iterable.type === 'string') {
|
|
331
|
+
// Iterate over characters
|
|
332
|
+
elements = [...iterable.value].map(c => makeString(c));
|
|
333
|
+
}
|
|
334
|
+
else if (iterable.type === 'object') {
|
|
335
|
+
// Iterate over keys
|
|
336
|
+
elements = [...iterable.properties.keys()].map(k => makeString(k));
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
throw createError('E400', `Cannot iterate over type '${iterable.type}'`, stmt.position, this.filePath);
|
|
340
|
+
}
|
|
341
|
+
for (const element of elements) {
|
|
278
342
|
const childScope = this.scope.createChild();
|
|
279
343
|
childScope.defineConst(stmt.variable, element);
|
|
280
344
|
const oldScope = this.scope;
|
|
@@ -389,6 +453,8 @@ export class Interpreter {
|
|
|
389
453
|
return makeString(expr.value);
|
|
390
454
|
case 'BoolLiteral':
|
|
391
455
|
return makeBool(expr.value);
|
|
456
|
+
case 'NullLiteral':
|
|
457
|
+
return makeNull();
|
|
392
458
|
case 'PitchLiteral':
|
|
393
459
|
return makePitch(expr.midi);
|
|
394
460
|
case 'DurLiteral':
|
|
@@ -396,7 +462,7 @@ export class Interpreter {
|
|
|
396
462
|
if (expr.numerator <= 0 || expr.denominator <= 0) {
|
|
397
463
|
throw new MFError('E101', `Invalid duration: ${expr.numerator}/${expr.denominator} (must be positive)`, expr.position, this.filePath);
|
|
398
464
|
}
|
|
399
|
-
return makeDur(expr.numerator, expr.denominator);
|
|
465
|
+
return makeDur(expr.numerator, expr.denominator, expr.dots);
|
|
400
466
|
case 'TimeLiteral':
|
|
401
467
|
return makeTime(expr.bar, expr.beat, expr.sub);
|
|
402
468
|
case 'ArrayLiteral': {
|
|
@@ -430,6 +496,10 @@ export class Interpreter {
|
|
|
430
496
|
return this.evaluateCall(expr.callee, expr.arguments, expr.position);
|
|
431
497
|
case 'IndexExpression': {
|
|
432
498
|
const obj = this.evaluate(expr.object);
|
|
499
|
+
// Optional chaining: return null if object is null
|
|
500
|
+
if (expr.optional && obj.type === 'null') {
|
|
501
|
+
return makeNull();
|
|
502
|
+
}
|
|
433
503
|
const idx = this.evaluate(expr.index);
|
|
434
504
|
if (obj.type === 'array') {
|
|
435
505
|
if (idx.type !== 'int') {
|
|
@@ -482,6 +552,10 @@ export class Interpreter {
|
|
|
482
552
|
}
|
|
483
553
|
case 'MemberExpression': {
|
|
484
554
|
const obj = this.evaluate(expr.object);
|
|
555
|
+
// Optional chaining: return null if object is null
|
|
556
|
+
if (expr.optional && obj.type === 'null') {
|
|
557
|
+
return makeNull();
|
|
558
|
+
}
|
|
485
559
|
if (obj.type === 'object') {
|
|
486
560
|
const value = obj.properties.get(expr.property);
|
|
487
561
|
return value ?? makeNull();
|
|
@@ -500,20 +574,59 @@ export class Interpreter {
|
|
|
500
574
|
throw createError('E400', 'Spread element not allowed here', expr.position, this.filePath);
|
|
501
575
|
case 'RangeExpression':
|
|
502
576
|
throw new Error(`${expr.kind} cannot be evaluated as expression`);
|
|
577
|
+
case 'ConditionalExpression':
|
|
578
|
+
// Ternary operator: condition ? consequent : alternate
|
|
579
|
+
return isTruthy(this.evaluate(expr.condition))
|
|
580
|
+
? this.evaluate(expr.consequent)
|
|
581
|
+
: this.evaluate(expr.alternate);
|
|
582
|
+
case 'TemplateLiteral': {
|
|
583
|
+
// Template literal: `Hello ${name}!`
|
|
584
|
+
let result = '';
|
|
585
|
+
for (let i = 0; i < expr.quasis.length; i++) {
|
|
586
|
+
result += expr.quasis[i];
|
|
587
|
+
if (i < expr.expressions.length) {
|
|
588
|
+
const val = this.evaluate(expr.expressions[i]);
|
|
589
|
+
result += toString(val);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return makeString(result);
|
|
593
|
+
}
|
|
594
|
+
case 'TypeofExpression': {
|
|
595
|
+
const val = this.evaluate(expr.operand);
|
|
596
|
+
return makeString(val.type);
|
|
597
|
+
}
|
|
503
598
|
default:
|
|
504
599
|
throw new Error(`Unknown expression kind: ${expr.kind}`);
|
|
505
600
|
}
|
|
506
601
|
}
|
|
507
602
|
evaluateBinary(op, leftExpr, rightExpr) {
|
|
508
|
-
|
|
509
|
-
const right = this.evaluate(rightExpr);
|
|
510
|
-
// Short-circuit evaluation for logical operators
|
|
603
|
+
// Short-circuit evaluation for logical operators - evaluate right side only if needed
|
|
511
604
|
if (op === '&&') {
|
|
512
|
-
|
|
605
|
+
const left = this.evaluate(leftExpr);
|
|
606
|
+
if (!isTruthy(left)) {
|
|
607
|
+
return makeBool(false);
|
|
608
|
+
}
|
|
609
|
+
const right = this.evaluate(rightExpr);
|
|
610
|
+
return makeBool(isTruthy(right));
|
|
513
611
|
}
|
|
514
612
|
if (op === '||') {
|
|
515
|
-
|
|
613
|
+
const left = this.evaluate(leftExpr);
|
|
614
|
+
if (isTruthy(left)) {
|
|
615
|
+
return makeBool(true);
|
|
616
|
+
}
|
|
617
|
+
const right = this.evaluate(rightExpr);
|
|
618
|
+
return makeBool(isTruthy(right));
|
|
619
|
+
}
|
|
620
|
+
// Nullish coalescing: return left if not null, else evaluate right
|
|
621
|
+
if (op === '??') {
|
|
622
|
+
const left = this.evaluate(leftExpr);
|
|
623
|
+
if (left.type !== 'null') {
|
|
624
|
+
return left;
|
|
625
|
+
}
|
|
626
|
+
return this.evaluate(rightExpr);
|
|
516
627
|
}
|
|
628
|
+
const left = this.evaluate(leftExpr);
|
|
629
|
+
const right = this.evaluate(rightExpr);
|
|
517
630
|
// Comparison operators
|
|
518
631
|
if (op === '==' || op === '!=') {
|
|
519
632
|
const equal = this.valuesEqual(left, right);
|
|
@@ -586,15 +699,21 @@ export class Interpreter {
|
|
|
586
699
|
if (op === '/') {
|
|
587
700
|
// Dur / Int
|
|
588
701
|
if (left.type === 'dur' && right.type === 'int') {
|
|
702
|
+
if (right.value === 0) {
|
|
703
|
+
throw createError('E400', 'Division by zero', leftExpr.position, this.filePath);
|
|
704
|
+
}
|
|
589
705
|
const den = left.denominator * right.value;
|
|
590
706
|
const gcd = this.gcd(left.numerator, den);
|
|
707
|
+
if (gcd === 0) {
|
|
708
|
+
throw createError('E400', 'Division by zero in duration calculation', leftExpr.position, this.filePath);
|
|
709
|
+
}
|
|
591
710
|
return makeDur(left.numerator / gcd, den / gcd);
|
|
592
711
|
}
|
|
593
712
|
// Numeric
|
|
594
713
|
const l = toNumber(left);
|
|
595
714
|
const r = toNumber(right);
|
|
596
715
|
if (r === 0) {
|
|
597
|
-
throw
|
|
716
|
+
throw createError('E400', 'Division by zero', rightExpr.position, this.filePath);
|
|
598
717
|
}
|
|
599
718
|
// Division always returns float unless both are int and divide evenly
|
|
600
719
|
if (left.type === 'int' && right.type === 'int' && l % r === 0) {
|
|
@@ -606,14 +725,40 @@ export class Interpreter {
|
|
|
606
725
|
const l = toNumber(left);
|
|
607
726
|
const r = toNumber(right);
|
|
608
727
|
if (r === 0) {
|
|
609
|
-
throw
|
|
728
|
+
throw createError('E400', 'Modulo by zero', rightExpr.position, this.filePath);
|
|
610
729
|
}
|
|
611
730
|
if (left.type === 'int' && right.type === 'int') {
|
|
612
731
|
return makeInt(l % r);
|
|
613
732
|
}
|
|
614
733
|
return makeFloat(l % r);
|
|
615
734
|
}
|
|
616
|
-
|
|
735
|
+
// Bitwise operators (only work on integers)
|
|
736
|
+
if (op === '&') {
|
|
737
|
+
const l = Math.trunc(toNumber(left));
|
|
738
|
+
const r = Math.trunc(toNumber(right));
|
|
739
|
+
return makeInt(l & r);
|
|
740
|
+
}
|
|
741
|
+
if (op === '|') {
|
|
742
|
+
const l = Math.trunc(toNumber(left));
|
|
743
|
+
const r = Math.trunc(toNumber(right));
|
|
744
|
+
return makeInt(l | r);
|
|
745
|
+
}
|
|
746
|
+
if (op === '^') {
|
|
747
|
+
const l = Math.trunc(toNumber(left));
|
|
748
|
+
const r = Math.trunc(toNumber(right));
|
|
749
|
+
return makeInt(l ^ r);
|
|
750
|
+
}
|
|
751
|
+
if (op === '<<') {
|
|
752
|
+
const l = Math.trunc(toNumber(left));
|
|
753
|
+
const r = Math.trunc(toNumber(right));
|
|
754
|
+
return makeInt(l << r);
|
|
755
|
+
}
|
|
756
|
+
if (op === '>>') {
|
|
757
|
+
const l = Math.trunc(toNumber(left));
|
|
758
|
+
const r = Math.trunc(toNumber(right));
|
|
759
|
+
return makeInt(l >> r);
|
|
760
|
+
}
|
|
761
|
+
throw createError('E400', `Unknown binary operator: ${op}`, leftExpr.position, this.filePath);
|
|
617
762
|
}
|
|
618
763
|
evaluateUnary(op, operandExpr) {
|
|
619
764
|
const operand = this.evaluate(operandExpr);
|
|
@@ -624,7 +769,11 @@ export class Interpreter {
|
|
|
624
769
|
const n = toNumber(operand);
|
|
625
770
|
return operand.type === 'float' ? makeFloat(-n) : makeInt(-n);
|
|
626
771
|
}
|
|
627
|
-
|
|
772
|
+
if (op === '~') {
|
|
773
|
+
const n = Math.trunc(toNumber(operand));
|
|
774
|
+
return makeInt(~n);
|
|
775
|
+
}
|
|
776
|
+
throw createError('E400', `Unknown unary operator: ${op}`, operandExpr.position, this.filePath);
|
|
628
777
|
}
|
|
629
778
|
evaluateCall(calleeExpr, args, position) {
|
|
630
779
|
// Helper to expand arguments (handle spread)
|
|
@@ -1299,15 +1448,17 @@ export class Interpreter {
|
|
|
1299
1448
|
throw createError('E400', 'substr() requires at least 2 arguments', position, this.filePath);
|
|
1300
1449
|
}
|
|
1301
1450
|
const str = this.evaluate(args[0]);
|
|
1302
|
-
const
|
|
1303
|
-
const length = args.length > 2 ? toNumber(this.evaluate(args[2])) : undefined;
|
|
1451
|
+
const startIdx = Math.floor(toNumber(this.evaluate(args[1])));
|
|
1452
|
+
const length = args.length > 2 ? Math.floor(toNumber(this.evaluate(args[2]))) : undefined;
|
|
1304
1453
|
if (str.type !== 'string') {
|
|
1305
1454
|
throw createError('E400', 'substr() first argument must be a string', position, this.filePath);
|
|
1306
1455
|
}
|
|
1456
|
+
// Handle negative start index (from end of string)
|
|
1457
|
+
const actualStart = startIdx < 0 ? Math.max(0, str.value.length + startIdx) : startIdx;
|
|
1307
1458
|
if (length !== undefined) {
|
|
1308
|
-
return makeString(str.value.
|
|
1459
|
+
return makeString(str.value.substring(actualStart, actualStart + length));
|
|
1309
1460
|
}
|
|
1310
|
-
return makeString(str.value.
|
|
1461
|
+
return makeString(str.value.substring(actualStart));
|
|
1311
1462
|
}
|
|
1312
1463
|
case 'indexOf': {
|
|
1313
1464
|
// indexOf(str, search) - returns index or -1
|
|
@@ -1568,6 +1719,12 @@ export class Interpreter {
|
|
|
1568
1719
|
if (step === 0) {
|
|
1569
1720
|
throw createError('E400', 'range() step cannot be 0', position, this.filePath);
|
|
1570
1721
|
}
|
|
1722
|
+
// Check iteration limit to prevent memory exhaustion
|
|
1723
|
+
const maxRangeSize = 1000000; // 1 million elements max
|
|
1724
|
+
const expectedSize = Math.abs(Math.ceil((end - start) / step));
|
|
1725
|
+
if (expectedSize > maxRangeSize) {
|
|
1726
|
+
throw createError('E401', `range() would create ${expectedSize} elements, exceeding limit of ${maxRangeSize}`, position, this.filePath);
|
|
1727
|
+
}
|
|
1571
1728
|
const result = [];
|
|
1572
1729
|
if (step > 0) {
|
|
1573
1730
|
for (let i = start; i < end; i += step) {
|
|
@@ -2306,10 +2463,10 @@ export class Interpreter {
|
|
|
2306
2463
|
// User-defined proc
|
|
2307
2464
|
const proc = this.scope.lookupProc(callee);
|
|
2308
2465
|
if (proc) {
|
|
2309
|
-
// Track recursion depth
|
|
2466
|
+
// Track recursion depth (relaxed from 100 to 1000)
|
|
2310
2467
|
const currentDepth = this.callStack.get(callee) || 0;
|
|
2311
|
-
if (currentDepth >=
|
|
2312
|
-
throw
|
|
2468
|
+
if (currentDepth >= 1000) {
|
|
2469
|
+
throw new MFError('E310', `Maximum recursion depth (1000) exceeded in '${callee}'`, position, this.filePath);
|
|
2313
2470
|
}
|
|
2314
2471
|
this.callStack.set(callee, currentDepth + 1);
|
|
2315
2472
|
// Create scope with parameters
|
|
@@ -2359,7 +2516,10 @@ export class Interpreter {
|
|
|
2359
2516
|
callFunctionValue(fn, evalArgs, position) {
|
|
2360
2517
|
// Create new scope chained to closure scope (not current scope!)
|
|
2361
2518
|
const callScope = fn.closure.createChild();
|
|
2362
|
-
// Bind parameters
|
|
2519
|
+
// Bind parameters (with default value support)
|
|
2520
|
+
// We need to evaluate defaults in the call scope so they can reference earlier params
|
|
2521
|
+
const oldScope = this.scope;
|
|
2522
|
+
this.scope = callScope;
|
|
2363
2523
|
for (let i = 0; i < fn.params.length; i++) {
|
|
2364
2524
|
const param = fn.params[i];
|
|
2365
2525
|
if (param.rest) {
|
|
@@ -2367,11 +2527,21 @@ export class Interpreter {
|
|
|
2367
2527
|
callScope.defineConst(param.name, makeArray(evalArgs.slice(i)));
|
|
2368
2528
|
break;
|
|
2369
2529
|
}
|
|
2370
|
-
|
|
2530
|
+
let value;
|
|
2531
|
+
if (i < evalArgs.length) {
|
|
2532
|
+
value = evalArgs[i];
|
|
2533
|
+
}
|
|
2534
|
+
else if (param.defaultValue) {
|
|
2535
|
+
// Evaluate default value in call scope (can reference earlier params)
|
|
2536
|
+
value = this.evaluate(param.defaultValue);
|
|
2537
|
+
}
|
|
2538
|
+
else {
|
|
2539
|
+
value = makeNull();
|
|
2540
|
+
}
|
|
2371
2541
|
callScope.defineConst(param.name, value);
|
|
2372
2542
|
}
|
|
2373
|
-
|
|
2374
|
-
|
|
2543
|
+
// Scope is already set to callScope for default value evaluation
|
|
2544
|
+
// Keep it that way for body execution
|
|
2375
2545
|
let result = makeNull();
|
|
2376
2546
|
try {
|
|
2377
2547
|
if (Array.isArray(fn.body)) {
|