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.
Files changed (61) 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/cli/commands/build.js +37 -7
  8. package/dist/cli/commands/build.js.map +1 -1
  9. package/dist/cli/commands/import.d.ts.map +1 -1
  10. package/dist/cli/commands/import.js +7 -0
  11. package/dist/cli/commands/import.js.map +1 -1
  12. package/dist/cli/commands/render.d.ts.map +1 -1
  13. package/dist/cli/commands/render.js +34 -2
  14. package/dist/cli/commands/render.js.map +1 -1
  15. package/dist/compiler/compiler.d.ts.map +1 -1
  16. package/dist/compiler/compiler.js +87 -15
  17. package/dist/compiler/compiler.js.map +1 -1
  18. package/dist/config/config.d.ts.map +1 -1
  19. package/dist/config/config.js +7 -1
  20. package/dist/config/config.js.map +1 -1
  21. package/dist/formatter/formatter.d.ts +1 -0
  22. package/dist/formatter/formatter.d.ts.map +1 -1
  23. package/dist/formatter/formatter.js +78 -8
  24. package/dist/formatter/formatter.js.map +1 -1
  25. package/dist/generators/midi.js +2 -1
  26. package/dist/generators/midi.js.map +1 -1
  27. package/dist/generators/musicxml.d.ts.map +1 -1
  28. package/dist/generators/musicxml.js +36 -24
  29. package/dist/generators/musicxml.js.map +1 -1
  30. package/dist/importers/musicxml.d.ts.map +1 -1
  31. package/dist/importers/musicxml.js +26 -10
  32. package/dist/importers/musicxml.js.map +1 -1
  33. package/dist/interpreter/builtins/midi.d.ts.map +1 -1
  34. package/dist/interpreter/builtins/midi.js +23 -0
  35. package/dist/interpreter/builtins/midi.js.map +1 -1
  36. package/dist/interpreter/interpreter.d.ts +2 -0
  37. package/dist/interpreter/interpreter.d.ts.map +1 -1
  38. package/dist/interpreter/interpreter.js +199 -29
  39. package/dist/interpreter/interpreter.js.map +1 -1
  40. package/dist/interpreter/runtime.d.ts +2 -1
  41. package/dist/interpreter/runtime.d.ts.map +1 -1
  42. package/dist/interpreter/runtime.js +6 -2
  43. package/dist/interpreter/runtime.js.map +1 -1
  44. package/dist/interpreter/scope.d.ts.map +1 -1
  45. package/dist/interpreter/scope.js +5 -4
  46. package/dist/interpreter/scope.js.map +1 -1
  47. package/dist/lexer/lexer.d.ts +4 -0
  48. package/dist/lexer/lexer.d.ts.map +1 -1
  49. package/dist/lexer/lexer.js +215 -14
  50. package/dist/lexer/lexer.js.map +1 -1
  51. package/dist/parser/parser.d.ts +19 -0
  52. package/dist/parser/parser.d.ts.map +1 -1
  53. package/dist/parser/parser.js +437 -30
  54. package/dist/parser/parser.js.map +1 -1
  55. package/dist/types/ast.d.ts +34 -3
  56. package/dist/types/ast.d.ts.map +1 -1
  57. package/dist/types/token.d.ts +25 -0
  58. package/dist/types/token.d.ts.map +1 -1
  59. package/dist/types/token.js +33 -0
  60. package/dist/types/token.js.map +1 -1
  61. 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
- const childScope = this.scope.createChild();
160
- const oldScope = this.scope;
161
- this.scope = childScope;
162
- this.executeStatements(stmt.alternate);
163
- this.scope = oldScope;
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
- if (iterable.type !== 'array') {
275
- throw createError('E400', 'For-each requires an array', stmt.position, this.filePath);
325
+ // Build the list of elements to iterate over
326
+ let elements = [];
327
+ if (iterable.type === 'array') {
328
+ elements = iterable.elements;
276
329
  }
277
- for (const element of iterable.elements) {
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
- const left = this.evaluate(leftExpr);
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
- return makeBool(isTruthy(left) && isTruthy(right));
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
- return makeBool(isTruthy(left) || isTruthy(right));
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 new Error('Division by zero');
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 new Error('Modulo by zero');
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
- throw new Error(`Unknown binary operator: ${op}`);
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
- throw new Error(`Unknown unary operator: ${op}`);
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 start = toNumber(this.evaluate(args[1]));
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.substr(start, length));
1459
+ return makeString(str.value.substring(actualStart, actualStart + length));
1309
1460
  }
1310
- return makeString(str.value.substr(start));
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 >= 100) {
2312
- throw createError('E310', `Maximum recursion depth exceeded in '${callee}'`, position, this.filePath);
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
- const value = i < evalArgs.length ? evalArgs[i] : makeNull();
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
- const oldScope = this.scope;
2374
- this.scope = callScope;
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)) {