redscript-mc 2.1.1 → 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 (47) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +50 -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__/mc-integration.test.js +25 -13
  7. package/dist/src/__tests__/schedule.test.js +105 -0
  8. package/dist/src/__tests__/typechecker.test.js +63 -0
  9. package/dist/src/emit/compile.js +1 -0
  10. package/dist/src/emit/index.js +3 -1
  11. package/dist/src/lir/lower.js +26 -0
  12. package/dist/src/mir/lower.js +341 -12
  13. package/dist/src/mir/types.d.ts +10 -0
  14. package/dist/src/optimizer/copy_prop.js +4 -0
  15. package/dist/src/optimizer/coroutine.d.ts +2 -0
  16. package/dist/src/optimizer/coroutine.js +33 -1
  17. package/dist/src/optimizer/dce.js +7 -1
  18. package/dist/src/optimizer/lir/const_imm.js +1 -1
  19. package/dist/src/optimizer/lir/dead_slot.js +1 -1
  20. package/dist/src/typechecker/index.d.ts +2 -0
  21. package/dist/src/typechecker/index.js +29 -0
  22. package/docs/ROADMAP.md +35 -0
  23. package/editors/vscode/package-lock.json +3 -3
  24. package/editors/vscode/package.json +1 -1
  25. package/examples/coroutine-demo.mcrs +11 -10
  26. package/jest.config.js +19 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/e2e/basic.test.ts +27 -0
  29. package/src/__tests__/e2e/coroutine.test.ts +23 -0
  30. package/src/__tests__/fixtures/array-test.mcrs +21 -22
  31. package/src/__tests__/fixtures/counter.mcrs +17 -0
  32. package/src/__tests__/fixtures/foreach-at-test.mcrs +9 -10
  33. package/src/__tests__/mc-integration.test.ts +25 -13
  34. package/src/__tests__/schedule.test.ts +112 -0
  35. package/src/__tests__/typechecker.test.ts +68 -0
  36. package/src/emit/compile.ts +1 -0
  37. package/src/emit/index.ts +3 -1
  38. package/src/lir/lower.ts +27 -0
  39. package/src/mir/lower.ts +355 -9
  40. package/src/mir/types.ts +4 -0
  41. package/src/optimizer/copy_prop.ts +4 -0
  42. package/src/optimizer/coroutine.ts +37 -1
  43. package/src/optimizer/dce.ts +6 -1
  44. package/src/optimizer/lir/const_imm.ts +1 -1
  45. package/src/optimizer/lir/dead_slot.ts +1 -1
  46. package/src/stdlib/timer.mcrs +10 -5
  47. package/src/typechecker/index.ts +39 -0
@@ -246,6 +246,69 @@ fn test() {
246
246
  expect(errors.length).toBeGreaterThan(0);
247
247
  expect(errors[0].message).toContain('Return type mismatch: expected void, got int');
248
248
  });
249
+ it('rejects setTimeout inside a loop', () => {
250
+ const errors = typeCheck(`
251
+ fn test() {
252
+ while (true) {
253
+ setTimeout(20, () => { say("x"); });
254
+ }
255
+ }
256
+ `);
257
+ expect(errors.length).toBeGreaterThan(0);
258
+ expect(errors[0].message).toContain('cannot be called inside a loop');
259
+ });
260
+ it('rejects setTimeout inside an if body', () => {
261
+ const errors = typeCheck(`
262
+ fn test() {
263
+ if (true) {
264
+ setTimeout(20, () => { say("x"); });
265
+ }
266
+ }
267
+ `);
268
+ expect(errors.length).toBeGreaterThan(0);
269
+ expect(errors[0].message).toContain('cannot be called inside an if/else body');
270
+ });
271
+ it('rejects setInterval inside a loop', () => {
272
+ const errors = typeCheck(`
273
+ fn test() {
274
+ while (true) {
275
+ setInterval(20, () => { say("x"); });
276
+ }
277
+ }
278
+ `);
279
+ expect(errors.length).toBeGreaterThan(0);
280
+ expect(errors[0].message).toContain('cannot be called inside a loop');
281
+ });
282
+ it('rejects Timer::new() inside a loop', () => {
283
+ const errors = typeCheck(`
284
+ struct Timer { _id: int, _duration: int }
285
+ impl Timer {
286
+ fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
287
+ }
288
+ fn test() {
289
+ while (true) {
290
+ let t: Timer = Timer::new(10);
291
+ }
292
+ }
293
+ `);
294
+ expect(errors.length).toBeGreaterThan(0);
295
+ expect(errors[0].message).toContain('Timer::new() cannot be called inside a loop');
296
+ });
297
+ it('rejects Timer::new() inside an if body', () => {
298
+ const errors = typeCheck(`
299
+ struct Timer { _id: int, _duration: int }
300
+ impl Timer {
301
+ fn new(duration: int) -> Timer { return { _id: 0, _duration: duration }; }
302
+ }
303
+ fn test() {
304
+ if (true) {
305
+ let t: Timer = Timer::new(10);
306
+ }
307
+ }
308
+ `);
309
+ expect(errors.length).toBeGreaterThan(0);
310
+ expect(errors[0].message).toContain('Timer::new() cannot be called inside an if/else body');
311
+ });
249
312
  it('allows impl instance methods with inferred self type', () => {
250
313
  const errors = typeCheck(`
251
314
  struct Timer { duration: int }
@@ -116,6 +116,7 @@ function compile(source, options = {}) {
116
116
  const coroResult = (0, coroutine_1.coroutineTransform)(mirOpt, coroutineInfos);
117
117
  const mirFinal = coroResult.module;
118
118
  tickFunctions.push(...coroResult.generatedTickFunctions);
119
+ warnings.push(...coroResult.warnings);
119
120
  // Stage 5: MIR → LIR
120
121
  const lir = (0, lower_3.lowerToLIR)(mirFinal);
121
122
  // Stage 6: LIR optimization
@@ -152,7 +152,9 @@ function emitInstr(instr, ns, obj, mcVersion) {
152
152
  return `execute unless score ${slot(instr.a)} ${cmpToMC(instr.op)} ${slot(instr.b)} run function ${instr.fn}`;
153
153
  case 'call_context': {
154
154
  const subcmds = instr.subcommands.map(emitSubcmd).join(' ');
155
- return `execute ${subcmds} run function ${instr.fn}`;
155
+ return subcmds
156
+ ? `execute ${subcmds} run function ${instr.fn}`
157
+ : `function ${instr.fn}`;
156
158
  }
157
159
  case 'return_value':
158
160
  return `scoreboard players operation $ret ${instr.slot.obj} = ${slot(instr.slot)}`;
@@ -275,6 +275,32 @@ function lowerInstrInner(instr, fn, ctx, instrs) {
275
275
  });
276
276
  break;
277
277
  }
278
+ case 'score_read': {
279
+ // execute store result score $dst __obj run scoreboard players get <player> <obj>
280
+ const dst = ctx.slot(instr.dst);
281
+ instrs.push({
282
+ kind: 'store_cmd_to_score',
283
+ dst,
284
+ cmd: { kind: 'raw', cmd: `scoreboard players get ${instr.player} ${instr.obj}` },
285
+ });
286
+ break;
287
+ }
288
+ case 'score_write': {
289
+ // Write a value to a vanilla MC scoreboard objective
290
+ if (instr.src.kind === 'const') {
291
+ instrs.push({ kind: 'raw', cmd: `scoreboard players set ${instr.player} ${instr.obj} ${instr.src.value}` });
292
+ }
293
+ else {
294
+ // execute store result score <player> <obj> run scoreboard players get $src __ns
295
+ const srcSlot = operandToSlot(instr.src, ctx, instrs);
296
+ instrs.push({
297
+ kind: 'store_cmd_to_score',
298
+ dst: { player: instr.player, obj: instr.obj },
299
+ cmd: { kind: 'raw', cmd: `scoreboard players get ${srcSlot.player} ${srcSlot.obj}` },
300
+ });
301
+ }
302
+ break;
303
+ }
278
304
  case 'call': {
279
305
  // Set parameter slots $p0, $p1, ...
280
306
  for (let i = 0; i < instr.args.length; i++) {
@@ -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`.