redscript-mc 2.1.0 → 2.2.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 (71) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +86 -21
  3. package/README.zh.md +61 -61
  4. package/dist/src/__tests__/e2e/basic.test.js +25 -0
  5. package/dist/src/__tests__/e2e/coroutine.test.js +22 -0
  6. package/dist/src/__tests__/lsp.test.js +76 -0
  7. package/dist/src/__tests__/mc-integration.test.js +25 -13
  8. package/dist/src/__tests__/mc-syntax.test.js +1 -6
  9. package/dist/src/__tests__/schedule.test.js +105 -0
  10. package/dist/src/__tests__/stdlib-include.test.d.ts +1 -0
  11. package/dist/src/__tests__/stdlib-include.test.js +86 -0
  12. package/dist/src/__tests__/typechecker.test.js +63 -0
  13. package/dist/src/cli.js +10 -3
  14. package/dist/src/compile.d.ts +1 -0
  15. package/dist/src/compile.js +33 -10
  16. package/dist/src/emit/compile.d.ts +2 -0
  17. package/dist/src/emit/compile.js +3 -2
  18. package/dist/src/emit/index.js +3 -1
  19. package/dist/src/lir/lower.js +26 -0
  20. package/dist/src/lsp/server.js +51 -0
  21. package/dist/src/mir/lower.js +341 -12
  22. package/dist/src/mir/types.d.ts +10 -0
  23. package/dist/src/optimizer/copy_prop.js +4 -0
  24. package/dist/src/optimizer/coroutine.d.ts +2 -0
  25. package/dist/src/optimizer/coroutine.js +33 -1
  26. package/dist/src/optimizer/dce.js +7 -1
  27. package/dist/src/optimizer/lir/const_imm.js +1 -1
  28. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  29. package/dist/src/typechecker/index.d.ts +2 -0
  30. package/dist/src/typechecker/index.js +29 -0
  31. package/docs/ROADMAP.md +35 -0
  32. package/editors/vscode/package-lock.json +3 -3
  33. package/editors/vscode/package.json +1 -1
  34. package/editors/vscode/syntaxes/redscript.tmLanguage.json +34 -0
  35. package/examples/coroutine-demo.mcrs +51 -0
  36. package/examples/enum-demo.mcrs +95 -0
  37. package/examples/scheduler-demo.mcrs +59 -0
  38. package/jest.config.js +19 -0
  39. package/package.json +1 -1
  40. package/src/__tests__/e2e/basic.test.ts +27 -0
  41. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  42. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  43. package/src/__tests__/fixtures/counter.mcrs +17 -0
  44. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  45. package/src/__tests__/lsp.test.ts +89 -0
  46. package/src/__tests__/mc-integration.test.ts +25 -13
  47. package/src/__tests__/mc-syntax.test.ts +1 -7
  48. package/src/__tests__/schedule.test.ts +112 -0
  49. package/src/__tests__/stdlib-include.test.ts +61 -0
  50. package/src/__tests__/typechecker.test.ts +68 -0
  51. package/src/cli.ts +9 -1
  52. package/src/compile.ts +44 -15
  53. package/src/emit/compile.ts +5 -2
  54. package/src/emit/index.ts +3 -1
  55. package/src/lir/lower.ts +27 -0
  56. package/src/lsp/server.ts +55 -0
  57. package/src/mir/lower.ts +355 -9
  58. package/src/mir/types.ts +4 -0
  59. package/src/optimizer/copy_prop.ts +4 -0
  60. package/src/optimizer/coroutine.ts +37 -1
  61. package/src/optimizer/dce.ts +6 -1
  62. package/src/optimizer/lir/const_imm.ts +1 -1
  63. package/src/optimizer/lir/dead_slot.ts +1 -1
  64. package/src/stdlib/timer.mcrs +10 -5
  65. package/src/typechecker/index.ts +39 -0
  66. package/examples/spiral.mcrs +0 -43
  67. package/src/examples/arena.mcrs +0 -44
  68. package/src/examples/counter.mcrs +0 -12
  69. package/src/examples/new_features_demo.mcrs +0 -193
  70. package/src/examples/rpg.mcrs +0 -13
  71. package/src/examples/stdlib_demo.mcrs +0 -181
@@ -48,15 +48,16 @@ function lowerToMIR(hir, sourceFile) {
48
48
  fnParamInfo.set(`${ib.typeName}::${m.name}`, m.params);
49
49
  }
50
50
  }
51
+ const timerCounter = { count: 0, timerId: 0 };
51
52
  const allFunctions = [];
52
53
  for (const f of hir.functions) {
53
- const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile);
54
+ const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter);
54
55
  allFunctions.push(fn, ...helpers);
55
56
  }
56
57
  // Lower impl block methods
57
58
  for (const ib of hir.implBlocks) {
58
59
  for (const m of ib.methods) {
59
- const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile);
60
+ const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter);
60
61
  allFunctions.push(fn, ...helpers);
61
62
  }
62
63
  }
@@ -70,7 +71,7 @@ function lowerToMIR(hir, sourceFile) {
70
71
  // Function lowering context
71
72
  // ---------------------------------------------------------------------------
72
73
  class FnContext {
73
- constructor(namespace, fnName, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map()) {
74
+ constructor(namespace, fnName, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), timerCounter = { count: 0, timerId: 0 }) {
74
75
  this.tempCounter = 0;
75
76
  this.blockCounter = 0;
76
77
  this.blocks = [];
@@ -82,10 +83,14 @@ class FnContext {
82
83
  this.structVars = new Map();
83
84
  /** Tuple variable tracking: varName → array of element temps (index = slot) */
84
85
  this.tupleVars = new Map();
86
+ /** Array variable tracking: varName → { ns, pathPrefix } for NBT-backed int[] */
87
+ this.arrayVars = new Map();
85
88
  /** Current source location (set during statement lowering) */
86
89
  this.currentSourceLoc = undefined;
87
90
  /** Source file path for the module being compiled */
88
91
  this.sourceFile = undefined;
92
+ /** Tracks temps whose values are known compile-time constants (for Timer static ID propagation) */
93
+ this.constTemps = new Map();
89
94
  this.namespace = namespace;
90
95
  this.fnName = fnName;
91
96
  this.structDefs = structDefs;
@@ -94,6 +99,7 @@ class FnContext {
94
99
  this.fnParamInfo = fnParamInfo;
95
100
  this.currentMacroParams = macroInfo.get(fnName)?.macroParams ?? new Set();
96
101
  this.enumDefs = enumDefs;
102
+ this.timerCounter = timerCounter;
97
103
  const entry = this.makeBlock('entry');
98
104
  this.currentBlock = entry;
99
105
  }
@@ -150,8 +156,8 @@ class FnContext {
150
156
  // ---------------------------------------------------------------------------
151
157
  // Function lowering
152
158
  // ---------------------------------------------------------------------------
153
- function lowerFunction(fn, namespace, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile) {
154
- const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs);
159
+ function lowerFunction(fn, namespace, structDefs = new Map(), implMethods = new Map(), macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile, timerCounter = { count: 0, timerId: 0 }) {
160
+ const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter);
155
161
  ctx.sourceFile = sourceFile;
156
162
  const fnMacroInfo = macroInfo.get(fn.name);
157
163
  // Create temps for parameters
@@ -184,9 +190,9 @@ function lowerFunction(fn, namespace, structDefs = new Map(), implMethods = new
184
190
  };
185
191
  return { fn: result, helpers: ctx.helperFunctions };
186
192
  }
187
- function lowerImplMethod(method, typeName, namespace, structDefs, implMethods, macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile) {
193
+ function lowerImplMethod(method, typeName, namespace, structDefs, implMethods, macroInfo = new Map(), fnParamInfo = new Map(), enumDefs = new Map(), sourceFile, timerCounter = { count: 0, timerId: 0 }) {
188
194
  const fnName = `${typeName}::${method.name}`;
189
- const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs);
195
+ const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter);
190
196
  ctx.sourceFile = sourceFile;
191
197
  const fields = structDefs.get(typeName) ?? [];
192
198
  const hasSelf = method.params.length > 0 && method.params[0].name === 'self';
@@ -345,6 +351,12 @@ function lowerStmt(stmt, ctx, scope) {
345
351
  const t = ctx.freshTemp();
346
352
  ctx.emit({ kind: 'copy', dst: t, src: { kind: 'temp', name: `__rf_${fieldName}` } });
347
353
  fieldTemps.set(fieldName, t);
354
+ // Propagate compile-time constants from return slots (e.g. Timer._id from Timer::new)
355
+ const rfSlot = `__rf_${fieldName}`;
356
+ const constVal = ctx.constTemps.get(rfSlot);
357
+ if (constVal !== undefined) {
358
+ ctx.constTemps.set(t, constVal);
359
+ }
348
360
  }
349
361
  ctx.structVars.set(stmt.name, { typeName: stmt.type.name, fields: fieldTemps });
350
362
  }
@@ -355,6 +367,35 @@ function lowerStmt(stmt, ctx, scope) {
355
367
  scope.set(stmt.name, t);
356
368
  }
357
369
  }
370
+ else if (stmt.init.kind === 'array_lit') {
371
+ // Array literal: write to NBT storage, track the var for index access
372
+ const ns = `${ctx.getNamespace()}:arrays`;
373
+ const pathPrefix = stmt.name;
374
+ ctx.arrayVars.set(stmt.name, { ns, pathPrefix });
375
+ const elems = stmt.init.elements;
376
+ // Check if all elements are pure integer literals (no side-effects)
377
+ const allConst = elems.every(e => e.kind === 'int_lit');
378
+ if (allConst) {
379
+ // Emit a single raw 'data modify ... set value [...]' to initialize the whole list
380
+ const vals = elems.map(e => e.value).join(', ');
381
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${vals}]`, args: [] });
382
+ }
383
+ else {
384
+ // Initialize with zeros, then overwrite dynamic elements
385
+ const zeros = elems.map(() => '0').join(', ');
386
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${zeros}]`, args: [] });
387
+ for (let i = 0; i < elems.length; i++) {
388
+ const elemOp = lowerExpr(elems[i], ctx, scope);
389
+ if (elemOp.kind !== 'const' || (elems[i].kind !== 'int_lit')) {
390
+ ctx.emit({ kind: 'nbt_write', ns, path: `${pathPrefix}[${i}]`, type: 'int', scale: 1, src: elemOp });
391
+ }
392
+ }
393
+ }
394
+ // Store array length as a temp in scope (for .len access)
395
+ const lenTemp = ctx.freshTemp();
396
+ ctx.emit({ kind: 'const', dst: lenTemp, value: elems.length });
397
+ scope.set(stmt.name, lenTemp);
398
+ }
358
399
  else {
359
400
  const valOp = lowerExpr(stmt.init, ctx, scope);
360
401
  const t = ctx.freshTemp();
@@ -616,6 +657,41 @@ function lowerStmt(stmt, ctx, scope) {
616
657
  ctx.terminate({ kind: 'jump', target: mergeBlock.id });
617
658
  }
618
659
  }
660
+ else if (arm.pattern.kind === 'range_lit') {
661
+ // Range pattern: e.g. 0..59 => emit ge/le comparisons
662
+ const range = arm.pattern.range;
663
+ const armBody = ctx.newBlock('match_arm');
664
+ const nextArm = ctx.newBlock('match_next');
665
+ // Chain checks: if min defined, check matchVal >= min; if max defined, check matchVal <= max
666
+ // Each failed check jumps to nextArm
667
+ const checks = [];
668
+ if (range.min !== undefined)
669
+ checks.push({ op: 'ge', bound: range.min });
670
+ if (range.max !== undefined)
671
+ checks.push({ op: 'le', bound: range.max });
672
+ if (checks.length === 0) {
673
+ // Open range — always matches
674
+ ctx.terminate({ kind: 'jump', target: armBody.id });
675
+ }
676
+ else {
677
+ // Emit checks sequentially; each check passes → continue to next or armBody
678
+ for (let ci = 0; ci < checks.length; ci++) {
679
+ const { op, bound } = checks[ci];
680
+ const cmpTemp = ctx.freshTemp();
681
+ ctx.emit({ kind: 'cmp', dst: cmpTemp, op, a: matchVal, b: { kind: 'const', value: bound } });
682
+ const passBlock = ci === checks.length - 1 ? armBody : ctx.newBlock('match_range_check');
683
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: cmpTemp }, then: passBlock.id, else: nextArm.id });
684
+ if (ci < checks.length - 1)
685
+ ctx.switchTo(passBlock);
686
+ }
687
+ }
688
+ ctx.switchTo(armBody);
689
+ lowerBlock(arm.body, ctx, new Map(scope));
690
+ if (isPlaceholderTerm(ctx.current().term)) {
691
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id });
692
+ }
693
+ ctx.switchTo(nextArm);
694
+ }
619
695
  else {
620
696
  const patOp = lowerExpr(arm.pattern, ctx, scope);
621
697
  const cmpTemp = ctx.freshTemp();
@@ -853,13 +929,97 @@ function lowerExpr(expr, ctx, scope) {
853
929
  return { kind: 'temp', name: t };
854
930
  }
855
931
  case 'index': {
932
+ // Check if obj is a tracked array variable with a constant index
933
+ if (expr.obj.kind === 'ident') {
934
+ const arrInfo = ctx.arrayVars.get(expr.obj.name);
935
+ if (arrInfo && expr.index.kind === 'int_lit') {
936
+ const t = ctx.freshTemp();
937
+ ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 });
938
+ return { kind: 'temp', name: t };
939
+ }
940
+ }
856
941
  const obj = lowerExpr(expr.obj, ctx, scope);
857
- const idx = lowerExpr(expr.index, ctx, scope);
942
+ lowerExpr(expr.index, ctx, scope);
858
943
  const t = ctx.freshTemp();
859
944
  ctx.emit({ kind: 'copy', dst: t, src: obj });
860
945
  return { kind: 'temp', name: t };
861
946
  }
862
947
  case 'call': {
948
+ // Handle scoreboard_get / score — read from vanilla MC scoreboard
949
+ if (expr.fn === 'scoreboard_get' || expr.fn === 'score') {
950
+ const player = hirExprToStringLiteral(expr.args[0]);
951
+ const obj = hirExprToStringLiteral(expr.args[1]);
952
+ const t = ctx.freshTemp();
953
+ ctx.emit({ kind: 'score_read', dst: t, player, obj });
954
+ return { kind: 'temp', name: t };
955
+ }
956
+ // Handle scoreboard_set — write to vanilla MC scoreboard
957
+ if (expr.fn === 'scoreboard_set') {
958
+ const player = hirExprToStringLiteral(expr.args[0]);
959
+ const obj = hirExprToStringLiteral(expr.args[1]);
960
+ const src = lowerExpr(expr.args[2], ctx, scope);
961
+ ctx.emit({ kind: 'score_write', player, obj, src });
962
+ const t = ctx.freshTemp();
963
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
964
+ return { kind: 'temp', name: t };
965
+ }
966
+ // Handle setTimeout/setInterval: lift lambda arg to a named helper function
967
+ if ((expr.fn === 'setTimeout' || expr.fn === 'setInterval') && expr.args.length === 2) {
968
+ const ticksArg = expr.args[0];
969
+ const callbackArg = expr.args[1];
970
+ const ns = ctx.getNamespace();
971
+ const id = ctx.timerCounter.count++;
972
+ const callbackName = `__timeout_callback_${id}`;
973
+ // Extract ticks value for the schedule command
974
+ let ticksLiteral = null;
975
+ if (ticksArg.kind === 'int_lit') {
976
+ ticksLiteral = ticksArg.value;
977
+ }
978
+ // Build the callback MIRFunction from the lambda body
979
+ if (callbackArg.kind === 'lambda') {
980
+ const cbCtx = new FnContext(ns, callbackName, ctx.structDefs, ctx.implMethods, ctx.macroInfo, ctx.fnParamInfo, ctx.enumDefs, ctx.timerCounter);
981
+ cbCtx.sourceFile = ctx.sourceFile;
982
+ const cbBody = Array.isArray(callbackArg.body) ? callbackArg.body : [{ kind: 'expr', expr: callbackArg.body }];
983
+ // For setInterval: reschedule at end of body
984
+ const bodyStmts = [...cbBody];
985
+ if (expr.fn === 'setInterval' && ticksLiteral !== null) {
986
+ // Append: raw `schedule function ns:callbackName ticksT`
987
+ bodyStmts.push({
988
+ kind: 'raw',
989
+ cmd: `schedule function ${ns}:${callbackName} ${ticksLiteral}t`,
990
+ });
991
+ }
992
+ lowerBlock(bodyStmts, cbCtx, new Map());
993
+ const cbCur = cbCtx.current();
994
+ if (isPlaceholderTerm(cbCur.term)) {
995
+ cbCtx.terminate({ kind: 'return', value: null });
996
+ }
997
+ const cbReachable = computeReachable(cbCtx.blocks, 'entry');
998
+ const cbLiveBlocks = cbCtx.blocks.filter(b => cbReachable.has(b.id));
999
+ computePreds(cbLiveBlocks);
1000
+ const cbFn = {
1001
+ name: callbackName,
1002
+ params: [],
1003
+ blocks: cbLiveBlocks,
1004
+ entry: 'entry',
1005
+ isMacro: false,
1006
+ };
1007
+ ctx.helperFunctions.push(cbFn, ...cbCtx.helperFunctions);
1008
+ }
1009
+ // Emit: schedule function ns:callbackName ticksT
1010
+ if (ticksLiteral !== null) {
1011
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} ${ticksLiteral}t`, args: [] });
1012
+ }
1013
+ else {
1014
+ // Dynamic ticks: lower ticks operand and emit a raw schedule (best-effort)
1015
+ const ticksOp = lowerExpr(ticksArg, ctx, scope);
1016
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} 1t`, args: [ticksOp] });
1017
+ }
1018
+ // setTimeout returns void (0), setInterval returns an int ID (0 for now)
1019
+ const t = ctx.freshTemp();
1020
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1021
+ return { kind: 'temp', name: t };
1022
+ }
863
1023
  // Handle builtin calls → raw MC commands
864
1024
  if (macro_1.BUILTIN_SET.has(expr.fn)) {
865
1025
  const cmd = formatBuiltinCall(expr.fn, expr.args, ctx.currentMacroParams);
@@ -872,6 +1032,14 @@ function lowerExpr(expr, ctx, scope) {
872
1032
  if (expr.args.length > 0 && expr.args[0].kind === 'ident') {
873
1033
  const sv = ctx.structVars.get(expr.args[0].name);
874
1034
  if (sv) {
1035
+ // Intercept Timer method calls when _id is a known compile-time constant
1036
+ if (sv.typeName === 'Timer') {
1037
+ const idTemp = sv.fields.get('_id');
1038
+ const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined;
1039
+ if (timerId !== undefined) {
1040
+ return lowerTimerMethod(expr.fn, timerId, sv, ctx, scope, expr.args.slice(1));
1041
+ }
1042
+ }
875
1043
  const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.fn);
876
1044
  if (methodInfo?.hasSelf) {
877
1045
  // Build args: self fields first, then remaining explicit args
@@ -921,6 +1089,14 @@ function lowerExpr(expr, ctx, scope) {
921
1089
  if (expr.callee.kind === 'member' && expr.callee.obj.kind === 'ident') {
922
1090
  const sv = ctx.structVars.get(expr.callee.obj.name);
923
1091
  if (sv) {
1092
+ // Intercept Timer method calls when _id is a known compile-time constant
1093
+ if (sv.typeName === 'Timer') {
1094
+ const idTemp = sv.fields.get('_id');
1095
+ const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined;
1096
+ if (timerId !== undefined) {
1097
+ return lowerTimerMethod(expr.callee.field, timerId, sv, ctx, scope, expr.args);
1098
+ }
1099
+ }
924
1100
  const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.callee.field);
925
1101
  if (methodInfo?.hasSelf) {
926
1102
  // Build args: self fields first, then explicit args
@@ -945,6 +1121,24 @@ function lowerExpr(expr, ctx, scope) {
945
1121
  return { kind: 'temp', name: t };
946
1122
  }
947
1123
  case 'static_call': {
1124
+ // Intercept Timer::new() to statically allocate a unique ID
1125
+ if (expr.type === 'Timer' && expr.method === 'new' && expr.args.length === 1) {
1126
+ const id = ctx.timerCounter.timerId++;
1127
+ const ns = ctx.getNamespace();
1128
+ const playerName = `__timer_${id}`;
1129
+ // Emit scoreboard initialization: ticks=0, active=0
1130
+ ctx.emit({ kind: 'score_write', player: `${playerName}_ticks`, obj: ns, src: { kind: 'const', value: 0 } });
1131
+ ctx.emit({ kind: 'score_write', player: `${playerName}_active`, obj: ns, src: { kind: 'const', value: 0 } });
1132
+ // Lower the duration argument
1133
+ const durationOp = lowerExpr(expr.args[0], ctx, scope);
1134
+ // Return fields via __rf_ slots (Timer has fields: _id, _duration)
1135
+ ctx.emit({ kind: 'const', dst: '__rf__id', value: id });
1136
+ ctx.constTemps.set('__rf__id', id);
1137
+ ctx.emit({ kind: 'copy', dst: '__rf__duration', src: durationOp });
1138
+ const t = ctx.freshTemp();
1139
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1140
+ return { kind: 'temp', name: t };
1141
+ }
948
1142
  const args = expr.args.map(a => lowerExpr(a, ctx, scope));
949
1143
  const t = ctx.freshTemp();
950
1144
  ctx.emit({ kind: 'call', dst: t, fn: `${expr.type}::${expr.method}`, args });
@@ -1024,6 +1218,98 @@ function lowerShortCircuitOr(expr, ctx, scope) {
1024
1218
  return { kind: 'temp', name: result };
1025
1219
  }
1026
1220
  // ---------------------------------------------------------------------------
1221
+ // Timer method inlining
1222
+ // ---------------------------------------------------------------------------
1223
+ /**
1224
+ * Inline a Timer instance method call using the statically-assigned timer ID.
1225
+ * Emits scoreboard operations directly, bypassing the Timer::* function calls.
1226
+ */
1227
+ function lowerTimerMethod(method, timerId, sv, ctx, scope, extraArgs) {
1228
+ const ns = ctx.getNamespace();
1229
+ const player = `__timer_${timerId}`;
1230
+ const t = ctx.freshTemp();
1231
+ if (method === 'start') {
1232
+ ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 1 } });
1233
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1234
+ }
1235
+ else if (method === 'pause') {
1236
+ ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 0 } });
1237
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1238
+ }
1239
+ else if (method === 'reset') {
1240
+ ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'const', value: 0 } });
1241
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1242
+ }
1243
+ else if (method === 'tick') {
1244
+ const durationTemp = sv.fields.get('_duration');
1245
+ const activeTemp = ctx.freshTemp();
1246
+ const ticksTemp = ctx.freshTemp();
1247
+ ctx.emit({ kind: 'score_read', dst: activeTemp, player: `${player}_active`, obj: ns });
1248
+ ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns });
1249
+ const innerThen = ctx.newBlock('timer_tick_inner');
1250
+ const innerMerge = ctx.newBlock('timer_tick_after_lt');
1251
+ const outerMerge = ctx.newBlock('timer_tick_done');
1252
+ const activeCheck = ctx.freshTemp();
1253
+ ctx.emit({ kind: 'cmp', op: 'eq', dst: activeCheck, a: { kind: 'temp', name: activeTemp }, b: { kind: 'const', value: 1 } });
1254
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: activeCheck }, then: innerThen.id, else: outerMerge.id });
1255
+ ctx.switchTo(innerThen);
1256
+ const lessCheck = ctx.freshTemp();
1257
+ if (durationTemp) {
1258
+ ctx.emit({ kind: 'cmp', op: 'lt', dst: lessCheck, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } });
1259
+ }
1260
+ else {
1261
+ ctx.emit({ kind: 'const', dst: lessCheck, value: 0 });
1262
+ }
1263
+ const doIncBlock = ctx.newBlock('timer_tick_inc');
1264
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: lessCheck }, then: doIncBlock.id, else: innerMerge.id });
1265
+ ctx.switchTo(doIncBlock);
1266
+ const newTicks = ctx.freshTemp();
1267
+ ctx.emit({ kind: 'add', dst: newTicks, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'const', value: 1 } });
1268
+ ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'temp', name: newTicks } });
1269
+ ctx.terminate({ kind: 'jump', target: innerMerge.id });
1270
+ ctx.switchTo(innerMerge);
1271
+ ctx.terminate({ kind: 'jump', target: outerMerge.id });
1272
+ ctx.switchTo(outerMerge);
1273
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1274
+ }
1275
+ else if (method === 'done') {
1276
+ const durationTemp = sv.fields.get('_duration');
1277
+ const ticksTemp = ctx.freshTemp();
1278
+ ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns });
1279
+ if (durationTemp) {
1280
+ ctx.emit({ kind: 'cmp', op: 'ge', dst: t, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } });
1281
+ }
1282
+ else {
1283
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1284
+ }
1285
+ }
1286
+ else if (method === 'elapsed') {
1287
+ ctx.emit({ kind: 'score_read', dst: t, player: `${player}_ticks`, obj: ns });
1288
+ }
1289
+ else if (method === 'remaining') {
1290
+ const durationTemp = sv.fields.get('_duration');
1291
+ const ticksTemp = ctx.freshTemp();
1292
+ ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns });
1293
+ if (durationTemp) {
1294
+ ctx.emit({ kind: 'sub', dst: t, a: { kind: 'temp', name: durationTemp }, b: { kind: 'temp', name: ticksTemp } });
1295
+ }
1296
+ else {
1297
+ ctx.emit({ kind: 'const', dst: t, value: 0 });
1298
+ }
1299
+ }
1300
+ else {
1301
+ // Unknown Timer method — emit regular call
1302
+ const fields = ['_id', '_duration'];
1303
+ const selfArgs = fields.map(f => {
1304
+ const temp = sv.fields.get(f);
1305
+ return temp ? { kind: 'temp', name: temp } : { kind: 'const', value: 0 };
1306
+ });
1307
+ const explicitArgs = extraArgs.map(a => lowerExpr(a, ctx, scope));
1308
+ ctx.emit({ kind: 'call', dst: t, fn: `Timer::${method}`, args: [...selfArgs, ...explicitArgs] });
1309
+ }
1310
+ return { kind: 'temp', name: t };
1311
+ }
1312
+ // ---------------------------------------------------------------------------
1027
1313
  // Execute subcommand lowering
1028
1314
  // ---------------------------------------------------------------------------
1029
1315
  function lowerExecuteSubcmd(sub) {
@@ -1128,13 +1414,38 @@ function formatBuiltinCall(fn, args, macroParams) {
1128
1414
  break;
1129
1415
  }
1130
1416
  case 'setblock': {
1131
- const [x, y, z, block] = strs;
1132
- cmd = `setblock ${x} ${y} ${z} ${block}`;
1417
+ // args: blockpos, block expand blockpos to x y z
1418
+ const [posOrX, blockOrY] = args;
1419
+ if (posOrX?.kind === 'blockpos') {
1420
+ const px = coordStr(posOrX.x);
1421
+ const py = coordStr(posOrX.y);
1422
+ const pz = coordStr(posOrX.z);
1423
+ const blk = exprToCommandArg(blockOrY, macroParams).str;
1424
+ cmd = `setblock ${px} ${py} ${pz} ${blk}`;
1425
+ }
1426
+ else {
1427
+ const [x, y, z, block] = strs;
1428
+ cmd = `setblock ${x} ${y} ${z} ${block}`;
1429
+ }
1133
1430
  break;
1134
1431
  }
1135
1432
  case 'fill': {
1136
- const [x1, y1, z1, x2, y2, z2, block] = strs;
1137
- cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`;
1433
+ // args: blockpos1, blockpos2, block expand both blockpos
1434
+ const [p1, p2, blkArg] = args;
1435
+ if (p1?.kind === 'blockpos' && p2?.kind === 'blockpos') {
1436
+ const x1 = coordStr(p1.x);
1437
+ const y1 = coordStr(p1.y);
1438
+ const z1 = coordStr(p1.z);
1439
+ const x2 = coordStr(p2.x);
1440
+ const y2 = coordStr(p2.y);
1441
+ const z2 = coordStr(p2.z);
1442
+ const blk = exprToCommandArg(blkArg, macroParams).str;
1443
+ cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${blk}`;
1444
+ }
1445
+ else {
1446
+ const [x1, y1, z1, x2, y2, z2, block] = strs;
1447
+ cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`;
1448
+ }
1138
1449
  break;
1139
1450
  }
1140
1451
  case 'say':
@@ -1217,6 +1528,14 @@ function formatBuiltinCall(fn, args, macroParams) {
1217
1528
  return hasMacro ? `${MACRO_SENTINEL}${cmd}` : cmd;
1218
1529
  }
1219
1530
  /** Convert an HIR expression to its MC command string representation */
1531
+ /** Convert a CoordComponent to a MC coordinate string */
1532
+ function coordStr(c) {
1533
+ switch (c.kind) {
1534
+ case 'absolute': return String(c.value);
1535
+ case 'relative': return c.offset === 0 ? '~' : `~${c.offset}`;
1536
+ case 'local': return c.offset === 0 ? '^' : `^${c.offset}`;
1537
+ }
1538
+ }
1220
1539
  function exprToCommandArg(expr, macroParams) {
1221
1540
  switch (expr.kind) {
1222
1541
  case 'int_lit': return { str: String(expr.value), isMacro: false };
@@ -1261,4 +1580,14 @@ function exprToCommandArg(expr, macroParams) {
1261
1580
  return { str: '~', isMacro: false };
1262
1581
  }
1263
1582
  }
1583
+ /** Extract a string literal from a HIR expression for use in MC commands */
1584
+ function hirExprToStringLiteral(expr) {
1585
+ switch (expr.kind) {
1586
+ case 'str_lit': return expr.value;
1587
+ case 'mc_name': return expr.value;
1588
+ case 'selector': return expr.raw;
1589
+ case 'int_lit': return String(expr.value);
1590
+ default: return '';
1591
+ }
1592
+ }
1264
1593
  //# sourceMappingURL=lower.js.map
@@ -137,6 +137,16 @@ export type MIRInstr = MIRInstrBase & ({
137
137
  type: NBTType;
138
138
  scale: number;
139
139
  src: Operand;
140
+ } | {
141
+ kind: 'score_read';
142
+ dst: Temp;
143
+ player: string;
144
+ obj: string;
145
+ } | {
146
+ kind: 'score_write';
147
+ player: string;
148
+ obj: string;
149
+ src: Operand;
140
150
  } | {
141
151
  kind: 'call';
142
152
  dst: Temp | null;
@@ -83,6 +83,8 @@ function rewriteUses(instr, copies) {
83
83
  return { ...instr, cond: resolve(instr.cond, copies) };
84
84
  case 'return':
85
85
  return { ...instr, value: instr.value ? resolve(instr.value, copies) : null };
86
+ case 'score_write':
87
+ return { ...instr, src: resolve(instr.src, copies) };
86
88
  default:
87
89
  return instr;
88
90
  }
@@ -106,6 +108,8 @@ function getDst(instr) {
106
108
  case 'call':
107
109
  case 'call_macro':
108
110
  return instr.dst;
111
+ case 'score_read':
112
+ return instr.dst;
109
113
  default:
110
114
  return null;
111
115
  }
@@ -25,6 +25,8 @@ export interface CoroutineResult {
25
25
  module: MIRModule;
26
26
  /** Names of generated @tick dispatcher functions (caller must add to tick list). */
27
27
  generatedTickFunctions: string[];
28
+ /** Warning messages for skipped transforms. */
29
+ warnings: string[];
28
30
  }
29
31
  /**
30
32
  * Apply the coroutine transform to all functions in `infos`.
@@ -25,16 +25,25 @@ exports.coroutineTransform = coroutineTransform;
25
25
  */
26
26
  function coroutineTransform(mod, infos) {
27
27
  if (infos.length === 0)
28
- return { module: mod, generatedTickFunctions: [] };
28
+ return { module: mod, generatedTickFunctions: [], warnings: [] };
29
29
  const infoMap = new Map(infos.map(i => [i.fnName, i]));
30
30
  const newFunctions = [];
31
31
  const tickFns = [];
32
+ const warnings = [];
32
33
  for (const fn of mod.functions) {
33
34
  const info = infoMap.get(fn.name);
34
35
  if (!info) {
35
36
  newFunctions.push(fn);
36
37
  continue;
37
38
  }
39
+ // Skip transform if function contains macro calls — continuations are called
40
+ // directly (not via `function ... with storage`) so macro variables like
41
+ // ${px} would not be substituted, causing MC parse errors.
42
+ if (fnContainsMacroCalls(fn)) {
43
+ warnings.push(`@coroutine cannot be applied to functions containing macro calls (skipped: ${fn.name})`);
44
+ newFunctions.push(fn);
45
+ continue;
46
+ }
38
47
  const transformed = transformCoroutine(fn, info, mod.objective);
39
48
  newFunctions.push(transformed.initFn);
40
49
  newFunctions.push(...transformed.continuations);
@@ -44,8 +53,31 @@ function coroutineTransform(mod, infos) {
44
53
  return {
45
54
  module: { ...mod, functions: newFunctions },
46
55
  generatedTickFunctions: tickFns,
56
+ warnings,
47
57
  };
48
58
  }
59
+ /**
60
+ * Returns true if any instruction in the function requires macro processing.
61
+ * This includes:
62
+ * - call_macro: explicit macro function invocations
63
+ * - call with fn = '__raw:\x01...': builtin calls (particle, summon, etc.) with macro params
64
+ * - call with fn = '__raw:<cmd>' where cmd contains '${': raw() commands with variable interpolation
65
+ */
66
+ function fnContainsMacroCalls(fn) {
67
+ for (const block of fn.blocks) {
68
+ for (const instr of [...block.instrs, block.term]) {
69
+ if (instr.kind === 'call_macro')
70
+ return true;
71
+ if (instr.kind === 'call' && instr.fn.startsWith('__raw:')) {
72
+ const cmd = instr.fn.slice(6);
73
+ // \x01 sentinel: builtin with macro params; '${': raw() with variable interpolation
74
+ if (cmd.startsWith('\x01') || cmd.includes('${'))
75
+ return true;
76
+ }
77
+ }
78
+ }
79
+ return false;
80
+ }
49
81
  function transformCoroutine(fn, info, objective) {
50
82
  const prefix = `_coro_${fn.name}`;
51
83
  const pcTemp = `${prefix}_pc`;
@@ -74,7 +74,8 @@ function recomputePreds(blocks) {
74
74
  }
75
75
  function hasSideEffects(instr) {
76
76
  if (instr.kind === 'call' || instr.kind === 'call_macro' ||
77
- instr.kind === 'call_context' || instr.kind === 'nbt_write')
77
+ instr.kind === 'call_context' || instr.kind === 'nbt_write' ||
78
+ instr.kind === 'score_write')
78
79
  return true;
79
80
  // Return field temps (__rf_) write to global return slots — not dead even if unused locally
80
81
  // Option slot temps (__opt_) write observable scoreboard state — preserve even if var unused
@@ -109,6 +110,8 @@ function getDst(instr) {
109
110
  case 'call':
110
111
  case 'call_macro':
111
112
  return instr.dst;
113
+ case 'score_read':
114
+ return instr.dst;
112
115
  default:
113
116
  return null;
114
117
  }
@@ -150,6 +153,9 @@ function getUsedTemps(instr) {
150
153
  if (instr.value)
151
154
  addOp(instr.value);
152
155
  break;
156
+ case 'score_write':
157
+ addOp(instr.src);
158
+ break;
153
159
  }
154
160
  return temps;
155
161
  }
@@ -27,7 +27,7 @@ function countSlotUses(instrs, target) {
27
27
  }
28
28
  function extractSlotsFromRaw(cmd) {
29
29
  const slots = [];
30
- const re = /(\$[\w.]+)\s+(\S+)/g;
30
+ const re = /(\$[\w.:]+)\s+(\S+)/g;
31
31
  let m;
32
32
  while ((m = re.exec(cmd)) !== null) {
33
33
  slots.push({ player: m[1], obj: m[2] });
@@ -24,7 +24,7 @@ function slotKey(s) {
24
24
  function extractSlotsFromRaw(cmd) {
25
25
  const slots = [];
26
26
  // Match $<player> <obj> patterns (scoreboard slot references)
27
- const re = /(\$[\w.]+)\s+(\S+)/g;
27
+ const re = /(\$[\w.:]+)\s+(\S+)/g;
28
28
  let m;
29
29
  while ((m = re.exec(cmd)) !== null) {
30
30
  slots.push({ player: m[1], obj: m[2] });
@@ -18,6 +18,8 @@ export declare class TypeChecker {
18
18
  private currentReturnType;
19
19
  private scope;
20
20
  private selfTypeStack;
21
+ private loopDepth;
22
+ private condDepth;
21
23
  private readonly richTextBuiltins;
22
24
  constructor(source?: string, filePath?: string);
23
25
  private getNodeLocation;