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
package/src/lsp/server.ts CHANGED
@@ -34,6 +34,7 @@ import { Parser } from '../parser'
34
34
  import { TypeChecker } from '../typechecker'
35
35
  import { DiagnosticError } from '../diagnostics'
36
36
  import type { Program, FnDecl, Span, TypeNode } from '../ast/types'
37
+ import { BUILTIN_METADATA } from '../builtins/metadata'
37
38
 
38
39
  // ---------------------------------------------------------------------------
39
40
  // Connection and document manager
@@ -123,6 +124,25 @@ function toDiagnostic(err: DiagnosticError): Diagnostic {
123
124
  }
124
125
  }
125
126
 
127
+ // ---------------------------------------------------------------------------
128
+ // Decorator hover docs
129
+ // ---------------------------------------------------------------------------
130
+
131
+ const DECORATOR_DOCS: Record<string, string> = {
132
+ tick: 'Runs every MC game tick (~20 Hz). No arguments.',
133
+ load: 'Runs on `/reload`. Use for initialization logic.',
134
+ coroutine: 'Splits loops into tick-spread continuations. Arg: `batch=N` (steps per tick, default 1).',
135
+ schedule: 'Schedules the function to run after N ticks. Arg: `ticks=N`.',
136
+ on_trigger: 'Runs when a trigger scoreboard objective is set by a player. Arg: trigger name.',
137
+ keep: 'Prevents the compiler from dead-code-eliminating this function.',
138
+ on: 'Generic event handler decorator.',
139
+ on_advancement: 'Runs when a player earns an advancement. Arg: advancement id.',
140
+ on_craft: 'Runs when a player crafts an item. Arg: item id.',
141
+ on_death: 'Runs when a player dies.',
142
+ on_join_team: 'Runs when a player joins a team. Arg: team name.',
143
+ on_login: 'Runs when a player logs in.',
144
+ }
145
+
126
146
  // ---------------------------------------------------------------------------
127
147
  // Hover helpers
128
148
  // ---------------------------------------------------------------------------
@@ -290,9 +310,44 @@ connection.onHover((params: TextDocumentPositionParams): Hover | null => {
290
310
  const program = cached?.program ?? null
291
311
  if (!program) return null
292
312
 
313
+ // Check if cursor is on a decorator (@tick, @load, etc.)
314
+ const lines = source.split('\n')
315
+ const lineText = lines[params.position.line] ?? ''
316
+ const decoratorMatch = lineText.match(/@([a-zA-Z_][a-zA-Z0-9_]*)/)
317
+ if (decoratorMatch) {
318
+ const ch = params.position.character
319
+ const atIdx = lineText.indexOf('@')
320
+ const decoratorEnd = atIdx + 1 + decoratorMatch[1].length
321
+ if (ch >= atIdx && ch <= decoratorEnd) {
322
+ const decoratorName = decoratorMatch[1]
323
+ const decoratorDoc = DECORATOR_DOCS[decoratorName]
324
+ if (decoratorDoc) {
325
+ const content: MarkupContent = {
326
+ kind: MarkupKind.Markdown,
327
+ value: `**@${decoratorName}** — ${decoratorDoc}`,
328
+ }
329
+ return { contents: content }
330
+ }
331
+ }
332
+ }
333
+
293
334
  const word = wordAt(source, params.position)
294
335
  if (!word) return null
295
336
 
337
+ // Check builtins
338
+ const builtin = BUILTIN_METADATA[word]
339
+ if (builtin) {
340
+ const paramStr = builtin.params
341
+ .map(p => `${p.name}: ${p.type}${p.required ? '' : '?'}`)
342
+ .join(', ')
343
+ const sig = `fn ${builtin.name}(${paramStr}): ${builtin.returns}`
344
+ const content: MarkupContent = {
345
+ kind: MarkupKind.Markdown,
346
+ value: `\`\`\`redscript\n${sig}\n\`\`\`\n${builtin.doc}`,
347
+ }
348
+ return { contents: content }
349
+ }
350
+
296
351
  // Check if it's a known function
297
352
  const fn = findFunction(program, word)
298
353
  if (fn) {
package/src/mir/lower.ts CHANGED
@@ -61,16 +61,18 @@ export function lowerToMIR(hir: HIRModule, sourceFile?: string): MIRModule {
61
61
  }
62
62
  }
63
63
 
64
+ const timerCounter = { count: 0, timerId: 0 }
65
+
64
66
  const allFunctions: MIRFunction[] = []
65
67
  for (const f of hir.functions) {
66
- const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile)
68
+ const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter)
67
69
  allFunctions.push(fn, ...helpers)
68
70
  }
69
71
 
70
72
  // Lower impl block methods
71
73
  for (const ib of hir.implBlocks) {
72
74
  for (const m of ib.methods) {
73
- const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile)
75
+ const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter)
74
76
  allFunctions.push(fn, ...helpers)
75
77
  }
76
78
  }
@@ -105,6 +107,8 @@ class FnContext {
105
107
  readonly structVars = new Map<string, { typeName: string; fields: Map<string, Temp> }>()
106
108
  /** Tuple variable tracking: varName → array of element temps (index = slot) */
107
109
  readonly tupleVars = new Map<string, Temp[]>()
110
+ /** Array variable tracking: varName → { ns, pathPrefix } for NBT-backed int[] */
111
+ readonly arrayVars = new Map<string, { ns: string; pathPrefix: string }>()
108
112
  /** Macro function info for all functions in the module */
109
113
  readonly macroInfo: Map<string, MacroFunctionInfo>
110
114
  /** Function parameter info for call_macro generation */
@@ -117,6 +121,10 @@ class FnContext {
117
121
  currentSourceLoc: SourceLoc | undefined = undefined
118
122
  /** Source file path for the module being compiled */
119
123
  sourceFile: string | undefined = undefined
124
+ /** Shared counter for setTimeout/setInterval callback naming and Timer static IDs (module-wide) */
125
+ readonly timerCounter: { count: number; timerId: number }
126
+ /** Tracks temps whose values are known compile-time constants (for Timer static ID propagation) */
127
+ readonly constTemps = new Map<Temp, number>()
120
128
 
121
129
  constructor(
122
130
  namespace: string,
@@ -126,6 +134,7 @@ class FnContext {
126
134
  macroInfo: Map<string, MacroFunctionInfo> = new Map(),
127
135
  fnParamInfo: Map<string, HIRParam[]> = new Map(),
128
136
  enumDefs: Map<string, Map<string, number>> = new Map(),
137
+ timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
129
138
  ) {
130
139
  this.namespace = namespace
131
140
  this.fnName = fnName
@@ -135,6 +144,7 @@ class FnContext {
135
144
  this.fnParamInfo = fnParamInfo
136
145
  this.currentMacroParams = macroInfo.get(fnName)?.macroParams ?? new Set()
137
146
  this.enumDefs = enumDefs
147
+ this.timerCounter = timerCounter
138
148
  const entry = this.makeBlock('entry')
139
149
  this.currentBlock = entry
140
150
  }
@@ -214,8 +224,9 @@ function lowerFunction(
214
224
  fnParamInfo: Map<string, HIRParam[]> = new Map(),
215
225
  enumDefs: Map<string, Map<string, number>> = new Map(),
216
226
  sourceFile?: string,
227
+ timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
217
228
  ): { fn: MIRFunction; helpers: MIRFunction[] } {
218
- const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs)
229
+ const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter)
219
230
  ctx.sourceFile = sourceFile
220
231
  const fnMacroInfo = macroInfo.get(fn.name)
221
232
 
@@ -267,9 +278,10 @@ function lowerImplMethod(
267
278
  fnParamInfo: Map<string, HIRParam[]> = new Map(),
268
279
  enumDefs: Map<string, Map<string, number>> = new Map(),
269
280
  sourceFile?: string,
281
+ timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
270
282
  ): { fn: MIRFunction; helpers: MIRFunction[] } {
271
283
  const fnName = `${typeName}::${method.name}`
272
- const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs)
284
+ const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter)
273
285
  ctx.sourceFile = sourceFile
274
286
  const fields = structDefs.get(typeName) ?? []
275
287
  const hasSelf = method.params.length > 0 && method.params[0].name === 'self'
@@ -444,6 +456,12 @@ function lowerStmt(
444
456
  const t = ctx.freshTemp()
445
457
  ctx.emit({ kind: 'copy', dst: t, src: { kind: 'temp', name: `__rf_${fieldName}` } })
446
458
  fieldTemps.set(fieldName, t)
459
+ // Propagate compile-time constants from return slots (e.g. Timer._id from Timer::new)
460
+ const rfSlot = `__rf_${fieldName}`
461
+ const constVal = ctx.constTemps.get(rfSlot)
462
+ if (constVal !== undefined) {
463
+ ctx.constTemps.set(t, constVal)
464
+ }
447
465
  }
448
466
  ctx.structVars.set(stmt.name, { typeName: stmt.type.name, fields: fieldTemps })
449
467
  } else {
@@ -452,6 +470,33 @@ function lowerStmt(
452
470
  ctx.emit({ kind: 'copy', dst: t, src: valOp })
453
471
  scope.set(stmt.name, t)
454
472
  }
473
+ } else if (stmt.init.kind === 'array_lit') {
474
+ // Array literal: write to NBT storage, track the var for index access
475
+ const ns = `${ctx.getNamespace()}:arrays`
476
+ const pathPrefix = stmt.name
477
+ ctx.arrayVars.set(stmt.name, { ns, pathPrefix })
478
+ const elems = stmt.init.elements
479
+ // Check if all elements are pure integer literals (no side-effects)
480
+ const allConst = elems.every(e => e.kind === 'int_lit')
481
+ if (allConst) {
482
+ // Emit a single raw 'data modify ... set value [...]' to initialize the whole list
483
+ const vals = elems.map(e => (e as { kind: 'int_lit'; value: number }).value).join(', ')
484
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${vals}]`, args: [] })
485
+ } else {
486
+ // Initialize with zeros, then overwrite dynamic elements
487
+ const zeros = elems.map(() => '0').join(', ')
488
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:data modify storage ${ns} ${pathPrefix} set value [${zeros}]`, args: [] })
489
+ for (let i = 0; i < elems.length; i++) {
490
+ const elemOp = lowerExpr(elems[i], ctx, scope)
491
+ if (elemOp.kind !== 'const' || (elems[i].kind !== 'int_lit')) {
492
+ ctx.emit({ kind: 'nbt_write', ns, path: `${pathPrefix}[${i}]`, type: 'int', scale: 1, src: elemOp })
493
+ }
494
+ }
495
+ }
496
+ // Store array length as a temp in scope (for .len access)
497
+ const lenTemp = ctx.freshTemp()
498
+ ctx.emit({ kind: 'const', dst: lenTemp, value: elems.length })
499
+ scope.set(stmt.name, lenTemp)
455
500
  } else {
456
501
  const valOp = lowerExpr(stmt.init, ctx, scope)
457
502
  const t = ctx.freshTemp()
@@ -730,6 +775,40 @@ function lowerStmt(
730
775
  if (isPlaceholderTerm(ctx.current().term)) {
731
776
  ctx.terminate({ kind: 'jump', target: mergeBlock.id })
732
777
  }
778
+ } else if (arm.pattern.kind === 'range_lit') {
779
+ // Range pattern: e.g. 0..59 => emit ge/le comparisons
780
+ const range = arm.pattern.range
781
+ const armBody = ctx.newBlock('match_arm')
782
+ const nextArm = ctx.newBlock('match_next')
783
+
784
+ // Chain checks: if min defined, check matchVal >= min; if max defined, check matchVal <= max
785
+ // Each failed check jumps to nextArm
786
+ const checks: Array<{ op: 'ge' | 'le'; bound: number }> = []
787
+ if (range.min !== undefined) checks.push({ op: 'ge', bound: range.min })
788
+ if (range.max !== undefined) checks.push({ op: 'le', bound: range.max })
789
+
790
+ if (checks.length === 0) {
791
+ // Open range — always matches
792
+ ctx.terminate({ kind: 'jump', target: armBody.id })
793
+ } else {
794
+ // Emit checks sequentially; each check passes → continue to next or armBody
795
+ for (let ci = 0; ci < checks.length; ci++) {
796
+ const { op, bound } = checks[ci]
797
+ const cmpTemp = ctx.freshTemp()
798
+ ctx.emit({ kind: 'cmp', dst: cmpTemp, op, a: matchVal, b: { kind: 'const', value: bound } })
799
+ const passBlock = ci === checks.length - 1 ? armBody : ctx.newBlock('match_range_check')
800
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: cmpTemp }, then: passBlock.id, else: nextArm.id })
801
+ if (ci < checks.length - 1) ctx.switchTo(passBlock)
802
+ }
803
+ }
804
+
805
+ ctx.switchTo(armBody)
806
+ lowerBlock(arm.body, ctx, new Map(scope))
807
+ if (isPlaceholderTerm(ctx.current().term)) {
808
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id })
809
+ }
810
+
811
+ ctx.switchTo(nextArm)
733
812
  } else {
734
813
  const patOp = lowerExpr(arm.pattern, ctx, scope)
735
814
  const cmpTemp = ctx.freshTemp()
@@ -996,14 +1075,116 @@ function lowerExpr(
996
1075
  }
997
1076
 
998
1077
  case 'index': {
1078
+ // Check if obj is a tracked array variable with a constant index
1079
+ if (expr.obj.kind === 'ident') {
1080
+ const arrInfo = ctx.arrayVars.get(expr.obj.name)
1081
+ if (arrInfo && expr.index.kind === 'int_lit') {
1082
+ const t = ctx.freshTemp()
1083
+ ctx.emit({ kind: 'nbt_read', dst: t, ns: arrInfo.ns, path: `${arrInfo.pathPrefix}[${expr.index.value}]`, scale: 1 })
1084
+ return { kind: 'temp', name: t }
1085
+ }
1086
+ }
999
1087
  const obj = lowerExpr(expr.obj, ctx, scope)
1000
- const idx = lowerExpr(expr.index, ctx, scope)
1088
+ lowerExpr(expr.index, ctx, scope)
1001
1089
  const t = ctx.freshTemp()
1002
1090
  ctx.emit({ kind: 'copy', dst: t, src: obj })
1003
1091
  return { kind: 'temp', name: t }
1004
1092
  }
1005
1093
 
1006
1094
  case 'call': {
1095
+ // Handle scoreboard_get / score — read from vanilla MC scoreboard
1096
+ if (expr.fn === 'scoreboard_get' || expr.fn === 'score') {
1097
+ const player = hirExprToStringLiteral(expr.args[0])
1098
+ const obj = hirExprToStringLiteral(expr.args[1])
1099
+ const t = ctx.freshTemp()
1100
+ ctx.emit({ kind: 'score_read', dst: t, player, obj })
1101
+ return { kind: 'temp', name: t }
1102
+ }
1103
+
1104
+ // Handle scoreboard_set — write to vanilla MC scoreboard
1105
+ if (expr.fn === 'scoreboard_set') {
1106
+ const player = hirExprToStringLiteral(expr.args[0])
1107
+ const obj = hirExprToStringLiteral(expr.args[1])
1108
+ const src = lowerExpr(expr.args[2], ctx, scope)
1109
+ ctx.emit({ kind: 'score_write', player, obj, src })
1110
+ const t = ctx.freshTemp()
1111
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1112
+ return { kind: 'temp', name: t }
1113
+ }
1114
+
1115
+ // Handle setTimeout/setInterval: lift lambda arg to a named helper function
1116
+ if ((expr.fn === 'setTimeout' || expr.fn === 'setInterval') && expr.args.length === 2) {
1117
+ const ticksArg = expr.args[0]
1118
+ const callbackArg = expr.args[1]
1119
+ const ns = ctx.getNamespace()
1120
+ const id = ctx.timerCounter.count++
1121
+ const callbackName = `__timeout_callback_${id}`
1122
+
1123
+ // Extract ticks value for the schedule command
1124
+ let ticksLiteral: number | null = null
1125
+ if (ticksArg.kind === 'int_lit') {
1126
+ ticksLiteral = ticksArg.value
1127
+ }
1128
+
1129
+ // Build the callback MIRFunction from the lambda body
1130
+ if (callbackArg.kind === 'lambda') {
1131
+ const cbCtx = new FnContext(
1132
+ ns,
1133
+ callbackName,
1134
+ ctx.structDefs,
1135
+ ctx.implMethods,
1136
+ ctx.macroInfo,
1137
+ ctx.fnParamInfo,
1138
+ ctx.enumDefs,
1139
+ ctx.timerCounter,
1140
+ )
1141
+ cbCtx.sourceFile = ctx.sourceFile
1142
+
1143
+ const cbBody = Array.isArray(callbackArg.body) ? callbackArg.body : [{ kind: 'expr' as const, expr: callbackArg.body }]
1144
+
1145
+ // For setInterval: reschedule at end of body
1146
+ const bodyStmts: typeof cbBody = [...cbBody]
1147
+ if (expr.fn === 'setInterval' && ticksLiteral !== null) {
1148
+ // Append: raw `schedule function ns:callbackName ticksT`
1149
+ bodyStmts.push({
1150
+ kind: 'raw' as const,
1151
+ cmd: `schedule function ${ns}:${callbackName} ${ticksLiteral}t`,
1152
+ } as any)
1153
+ }
1154
+
1155
+ lowerBlock(bodyStmts, cbCtx, new Map())
1156
+ const cbCur = cbCtx.current()
1157
+ if (isPlaceholderTerm(cbCur.term)) {
1158
+ cbCtx.terminate({ kind: 'return', value: null })
1159
+ }
1160
+ const cbReachable = computeReachable(cbCtx.blocks, 'entry')
1161
+ const cbLiveBlocks = cbCtx.blocks.filter(b => cbReachable.has(b.id))
1162
+ computePreds(cbLiveBlocks)
1163
+ const cbFn: MIRFunction = {
1164
+ name: callbackName,
1165
+ params: [],
1166
+ blocks: cbLiveBlocks,
1167
+ entry: 'entry',
1168
+ isMacro: false,
1169
+ }
1170
+ ctx.helperFunctions.push(cbFn, ...cbCtx.helperFunctions)
1171
+ }
1172
+
1173
+ // Emit: schedule function ns:callbackName ticksT
1174
+ if (ticksLiteral !== null) {
1175
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} ${ticksLiteral}t`, args: [] })
1176
+ } else {
1177
+ // Dynamic ticks: lower ticks operand and emit a raw schedule (best-effort)
1178
+ const ticksOp = lowerExpr(ticksArg, ctx, scope)
1179
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:schedule function ${ns}:${callbackName} 1t`, args: [ticksOp] })
1180
+ }
1181
+
1182
+ // setTimeout returns void (0), setInterval returns an int ID (0 for now)
1183
+ const t = ctx.freshTemp()
1184
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1185
+ return { kind: 'temp', name: t }
1186
+ }
1187
+
1007
1188
  // Handle builtin calls → raw MC commands
1008
1189
  if (BUILTIN_SET.has(expr.fn)) {
1009
1190
  const cmd = formatBuiltinCall(expr.fn, expr.args, ctx.currentMacroParams)
@@ -1017,6 +1198,14 @@ function lowerExpr(
1017
1198
  if (expr.args.length > 0 && expr.args[0].kind === 'ident') {
1018
1199
  const sv = ctx.structVars.get(expr.args[0].name)
1019
1200
  if (sv) {
1201
+ // Intercept Timer method calls when _id is a known compile-time constant
1202
+ if (sv.typeName === 'Timer') {
1203
+ const idTemp = sv.fields.get('_id')
1204
+ const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined
1205
+ if (timerId !== undefined) {
1206
+ return lowerTimerMethod(expr.fn, timerId, sv, ctx, scope, expr.args.slice(1))
1207
+ }
1208
+ }
1020
1209
  const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.fn)
1021
1210
  if (methodInfo?.hasSelf) {
1022
1211
  // Build args: self fields first, then remaining explicit args
@@ -1069,6 +1258,14 @@ function lowerExpr(
1069
1258
  if (expr.callee.kind === 'member' && expr.callee.obj.kind === 'ident') {
1070
1259
  const sv = ctx.structVars.get(expr.callee.obj.name)
1071
1260
  if (sv) {
1261
+ // Intercept Timer method calls when _id is a known compile-time constant
1262
+ if (sv.typeName === 'Timer') {
1263
+ const idTemp = sv.fields.get('_id')
1264
+ const timerId = idTemp !== undefined ? ctx.constTemps.get(idTemp) : undefined
1265
+ if (timerId !== undefined) {
1266
+ return lowerTimerMethod(expr.callee.field, timerId, sv, ctx, scope, expr.args)
1267
+ }
1268
+ }
1072
1269
  const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.callee.field)
1073
1270
  if (methodInfo?.hasSelf) {
1074
1271
  // Build args: self fields first, then explicit args
@@ -1094,6 +1291,24 @@ function lowerExpr(
1094
1291
  }
1095
1292
 
1096
1293
  case 'static_call': {
1294
+ // Intercept Timer::new() to statically allocate a unique ID
1295
+ if (expr.type === 'Timer' && expr.method === 'new' && expr.args.length === 1) {
1296
+ const id = ctx.timerCounter.timerId++
1297
+ const ns = ctx.getNamespace()
1298
+ const playerName = `__timer_${id}`
1299
+ // Emit scoreboard initialization: ticks=0, active=0
1300
+ ctx.emit({ kind: 'score_write', player: `${playerName}_ticks`, obj: ns, src: { kind: 'const', value: 0 } })
1301
+ ctx.emit({ kind: 'score_write', player: `${playerName}_active`, obj: ns, src: { kind: 'const', value: 0 } })
1302
+ // Lower the duration argument
1303
+ const durationOp = lowerExpr(expr.args[0], ctx, scope)
1304
+ // Return fields via __rf_ slots (Timer has fields: _id, _duration)
1305
+ ctx.emit({ kind: 'const', dst: '__rf__id', value: id })
1306
+ ctx.constTemps.set('__rf__id', id)
1307
+ ctx.emit({ kind: 'copy', dst: '__rf__duration', src: durationOp })
1308
+ const t = ctx.freshTemp()
1309
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1310
+ return { kind: 'temp', name: t }
1311
+ }
1097
1312
  const args = expr.args.map(a => lowerExpr(a, ctx, scope))
1098
1313
  const t = ctx.freshTemp()
1099
1314
  ctx.emit({ kind: 'call', dst: t, fn: `${expr.type}::${expr.method}`, args })
@@ -1198,6 +1413,98 @@ function lowerShortCircuitOr(
1198
1413
  return { kind: 'temp', name: result }
1199
1414
  }
1200
1415
 
1416
+ // ---------------------------------------------------------------------------
1417
+ // Timer method inlining
1418
+ // ---------------------------------------------------------------------------
1419
+
1420
+ /**
1421
+ * Inline a Timer instance method call using the statically-assigned timer ID.
1422
+ * Emits scoreboard operations directly, bypassing the Timer::* function calls.
1423
+ */
1424
+ function lowerTimerMethod(
1425
+ method: string,
1426
+ timerId: number,
1427
+ sv: { typeName: string; fields: Map<string, Temp> },
1428
+ ctx: FnContext,
1429
+ scope: Map<string, Temp>,
1430
+ extraArgs: HIRExpr[],
1431
+ ): Operand {
1432
+ const ns = ctx.getNamespace()
1433
+ const player = `__timer_${timerId}`
1434
+ const t = ctx.freshTemp()
1435
+
1436
+ if (method === 'start') {
1437
+ ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 1 } })
1438
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1439
+ } else if (method === 'pause') {
1440
+ ctx.emit({ kind: 'score_write', player: `${player}_active`, obj: ns, src: { kind: 'const', value: 0 } })
1441
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1442
+ } else if (method === 'reset') {
1443
+ ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'const', value: 0 } })
1444
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1445
+ } else if (method === 'tick') {
1446
+ const durationTemp = sv.fields.get('_duration')
1447
+ const activeTemp = ctx.freshTemp()
1448
+ const ticksTemp = ctx.freshTemp()
1449
+ ctx.emit({ kind: 'score_read', dst: activeTemp, player: `${player}_active`, obj: ns })
1450
+ ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns })
1451
+ const innerThen = ctx.newBlock('timer_tick_inner')
1452
+ const innerMerge = ctx.newBlock('timer_tick_after_lt')
1453
+ const outerMerge = ctx.newBlock('timer_tick_done')
1454
+ const activeCheck = ctx.freshTemp()
1455
+ ctx.emit({ kind: 'cmp', op: 'eq', dst: activeCheck, a: { kind: 'temp', name: activeTemp }, b: { kind: 'const', value: 1 } })
1456
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: activeCheck }, then: innerThen.id, else: outerMerge.id })
1457
+ ctx.switchTo(innerThen)
1458
+ const lessCheck = ctx.freshTemp()
1459
+ if (durationTemp) {
1460
+ ctx.emit({ kind: 'cmp', op: 'lt', dst: lessCheck, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } })
1461
+ } else {
1462
+ ctx.emit({ kind: 'const', dst: lessCheck, value: 0 })
1463
+ }
1464
+ const doIncBlock = ctx.newBlock('timer_tick_inc')
1465
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: lessCheck }, then: doIncBlock.id, else: innerMerge.id })
1466
+ ctx.switchTo(doIncBlock)
1467
+ const newTicks = ctx.freshTemp()
1468
+ ctx.emit({ kind: 'add', dst: newTicks, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'const', value: 1 } })
1469
+ ctx.emit({ kind: 'score_write', player: `${player}_ticks`, obj: ns, src: { kind: 'temp', name: newTicks } })
1470
+ ctx.terminate({ kind: 'jump', target: innerMerge.id })
1471
+ ctx.switchTo(innerMerge)
1472
+ ctx.terminate({ kind: 'jump', target: outerMerge.id })
1473
+ ctx.switchTo(outerMerge)
1474
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1475
+ } else if (method === 'done') {
1476
+ const durationTemp = sv.fields.get('_duration')
1477
+ const ticksTemp = ctx.freshTemp()
1478
+ ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns })
1479
+ if (durationTemp) {
1480
+ ctx.emit({ kind: 'cmp', op: 'ge', dst: t, a: { kind: 'temp', name: ticksTemp }, b: { kind: 'temp', name: durationTemp } })
1481
+ } else {
1482
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1483
+ }
1484
+ } else if (method === 'elapsed') {
1485
+ ctx.emit({ kind: 'score_read', dst: t, player: `${player}_ticks`, obj: ns })
1486
+ } else if (method === 'remaining') {
1487
+ const durationTemp = sv.fields.get('_duration')
1488
+ const ticksTemp = ctx.freshTemp()
1489
+ ctx.emit({ kind: 'score_read', dst: ticksTemp, player: `${player}_ticks`, obj: ns })
1490
+ if (durationTemp) {
1491
+ ctx.emit({ kind: 'sub', dst: t, a: { kind: 'temp', name: durationTemp }, b: { kind: 'temp', name: ticksTemp } })
1492
+ } else {
1493
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
1494
+ }
1495
+ } else {
1496
+ // Unknown Timer method — emit regular call
1497
+ const fields = ['_id', '_duration']
1498
+ const selfArgs: Operand[] = fields.map(f => {
1499
+ const temp = sv.fields.get(f)
1500
+ return temp ? { kind: 'temp' as const, name: temp } : { kind: 'const' as const, value: 0 }
1501
+ })
1502
+ const explicitArgs = extraArgs.map(a => lowerExpr(a, ctx, scope))
1503
+ ctx.emit({ kind: 'call', dst: t, fn: `Timer::${method}`, args: [...selfArgs, ...explicitArgs] })
1504
+ }
1505
+ return { kind: 'temp', name: t }
1506
+ }
1507
+
1201
1508
  // ---------------------------------------------------------------------------
1202
1509
  // Execute subcommand lowering
1203
1510
  // ---------------------------------------------------------------------------
@@ -1310,13 +1617,32 @@ function formatBuiltinCall(
1310
1617
  break
1311
1618
  }
1312
1619
  case 'setblock': {
1313
- const [x, y, z, block] = strs
1314
- cmd = `setblock ${x} ${y} ${z} ${block}`
1620
+ // args: blockpos, block expand blockpos to x y z
1621
+ const [posOrX, blockOrY] = args
1622
+ if (posOrX?.kind === 'blockpos') {
1623
+ const px = coordStr(posOrX.x)
1624
+ const py = coordStr(posOrX.y)
1625
+ const pz = coordStr(posOrX.z)
1626
+ const blk = exprToCommandArg(blockOrY, macroParams).str
1627
+ cmd = `setblock ${px} ${py} ${pz} ${blk}`
1628
+ } else {
1629
+ const [x, y, z, block] = strs
1630
+ cmd = `setblock ${x} ${y} ${z} ${block}`
1631
+ }
1315
1632
  break
1316
1633
  }
1317
1634
  case 'fill': {
1318
- const [x1, y1, z1, x2, y2, z2, block] = strs
1319
- cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`
1635
+ // args: blockpos1, blockpos2, block expand both blockpos
1636
+ const [p1, p2, blkArg] = args
1637
+ if (p1?.kind === 'blockpos' && p2?.kind === 'blockpos') {
1638
+ const x1 = coordStr(p1.x); const y1 = coordStr(p1.y); const z1 = coordStr(p1.z)
1639
+ const x2 = coordStr(p2.x); const y2 = coordStr(p2.y); const z2 = coordStr(p2.z)
1640
+ const blk = exprToCommandArg(blkArg, macroParams).str
1641
+ cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${blk}`
1642
+ } else {
1643
+ const [x1, y1, z1, x2, y2, z2, block] = strs
1644
+ cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`
1645
+ }
1320
1646
  break
1321
1647
  }
1322
1648
  case 'say': cmd = `say ${strs[0] ?? ''}`; break
@@ -1355,6 +1681,15 @@ function formatBuiltinCall(
1355
1681
  }
1356
1682
 
1357
1683
  /** Convert an HIR expression to its MC command string representation */
1684
+ /** Convert a CoordComponent to a MC coordinate string */
1685
+ function coordStr(c: import('../ast/types').CoordComponent): string {
1686
+ switch (c.kind) {
1687
+ case 'absolute': return String(c.value)
1688
+ case 'relative': return c.offset === 0 ? '~' : `~${c.offset}`
1689
+ case 'local': return c.offset === 0 ? '^' : `^${c.offset}`
1690
+ }
1691
+ }
1692
+
1358
1693
  function exprToCommandArg(
1359
1694
  expr: HIRExpr,
1360
1695
  macroParams: Set<string>,
@@ -1401,3 +1736,14 @@ function exprToCommandArg(
1401
1736
  return { str: '~', isMacro: false }
1402
1737
  }
1403
1738
  }
1739
+
1740
+ /** Extract a string literal from a HIR expression for use in MC commands */
1741
+ function hirExprToStringLiteral(expr: HIRExpr): string {
1742
+ switch (expr.kind) {
1743
+ case 'str_lit': return expr.value
1744
+ case 'mc_name': return expr.value
1745
+ case 'selector': return expr.raw
1746
+ case 'int_lit': return String(expr.value)
1747
+ default: return ''
1748
+ }
1749
+ }
package/src/mir/types.ts CHANGED
@@ -80,6 +80,10 @@ export type MIRInstr = MIRInstrBase & (
80
80
  | { kind: 'nbt_read'; dst: Temp; ns: string; path: string; scale: number }
81
81
  | { kind: 'nbt_write'; ns: string; path: string; type: NBTType; scale: number; src: Operand }
82
82
 
83
+ // ── Vanilla scoreboard interop ────────────────────────────────────────────
84
+ | { kind: 'score_read'; dst: Temp; player: string; obj: string }
85
+ | { kind: 'score_write'; player: string; obj: string; src: Operand }
86
+
83
87
  // ── Function calls ────────────────────────────────────────────────────────
84
88
  | { kind: 'call'; dst: Temp | null; fn: string; args: Operand[] }
85
89
  | { kind: 'call_macro'; dst: Temp | null; fn: string; args: { name: string; value: Operand; type: NBTType; scale: number }[] }
@@ -85,6 +85,8 @@ function rewriteUses(instr: MIRInstr, copies: Map<Temp, Operand>): MIRInstr {
85
85
  return { ...instr, cond: resolve(instr.cond, copies) }
86
86
  case 'return':
87
87
  return { ...instr, value: instr.value ? resolve(instr.value, copies) : null }
88
+ case 'score_write':
89
+ return { ...instr, src: resolve(instr.src, copies) }
88
90
  default:
89
91
  return instr
90
92
  }
@@ -100,6 +102,8 @@ function getDst(instr: MIRInstr): Temp | null {
100
102
  return instr.dst
101
103
  case 'call': case 'call_macro':
102
104
  return instr.dst
105
+ case 'score_read':
106
+ return instr.dst
103
107
  default:
104
108
  return null
105
109
  }