redscript-mc 3.0.1 → 3.0.2

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 (225) hide show
  1. package/.github/workflows/ci.yml +1 -0
  2. package/README.md +119 -313
  3. package/README.zh.md +118 -314
  4. package/ROADMAP.md +5 -5
  5. package/dist/data/impl_test/function/counter/get.mcfunction +5 -0
  6. package/dist/data/impl_test/function/counter/inc.mcfunction +7 -0
  7. package/dist/data/impl_test/function/counter/new.mcfunction +4 -0
  8. package/dist/data/impl_test/function/load.mcfunction +1 -0
  9. package/dist/data/impl_test/function/test_impl.mcfunction +10 -0
  10. package/dist/data/minecraft/tags/function/load.json +5 -0
  11. package/dist/data/playground/function/load.mcfunction +1 -0
  12. package/dist/data/playground/function/start.mcfunction +4 -0
  13. package/dist/data/playground/function/start__say_macro_t1.mcfunction +1 -0
  14. package/dist/data/playground/function/stop.mcfunction +5 -0
  15. package/dist/data/playground/function/stop__say_macro_t0.mcfunction +1 -0
  16. package/dist/data/stdlib_queue8_test/function/__queue_append_apply.mcfunction +4 -0
  17. package/dist/data/stdlib_queue8_test/function/__queue_peek_apply.mcfunction +4 -0
  18. package/dist/data/stdlib_queue8_test/function/__queue_size_raw_apply.mcfunction +4 -0
  19. package/dist/data/stdlib_queue8_test/function/load.mcfunction +1 -0
  20. package/dist/data/stdlib_queue8_test/function/queue_clear.mcfunction +6 -0
  21. package/dist/data/stdlib_queue8_test/function/queue_empty__merge_1.mcfunction +5 -0
  22. package/dist/data/stdlib_queue8_test/function/queue_empty__then_0.mcfunction +5 -0
  23. package/dist/data/stdlib_queue8_test/function/queue_peek__merge_1.mcfunction +13 -0
  24. package/dist/data/stdlib_queue8_test/function/queue_peek__then_0.mcfunction +5 -0
  25. package/dist/data/stdlib_queue8_test/function/queue_pop__merge_1.mcfunction +15 -0
  26. package/dist/data/stdlib_queue8_test/function/queue_pop__then_0.mcfunction +5 -0
  27. package/dist/data/stdlib_queue8_test/function/queue_push__const_11.mcfunction +6 -0
  28. package/dist/data/stdlib_queue8_test/function/queue_push__const_22.mcfunction +6 -0
  29. package/dist/data/stdlib_queue8_test/function/queue_size.mcfunction +13 -0
  30. package/dist/data/stdlib_queue8_test/function/test_queue_push_and_size.mcfunction +13 -0
  31. package/dist/data/test/function/load.mcfunction +1 -0
  32. package/dist/data/test/function/say_at.mcfunction +6 -0
  33. package/dist/data/test/function/test.mcfunction +4 -0
  34. package/dist/pack.mcmeta +6 -0
  35. package/dist/package.json +1 -1
  36. package/dist/src/__tests__/formatter-extra.test.d.ts +7 -0
  37. package/dist/src/__tests__/formatter-extra.test.js +123 -0
  38. package/dist/src/__tests__/global-vars.test.d.ts +13 -0
  39. package/dist/src/__tests__/global-vars.test.js +156 -0
  40. package/dist/src/__tests__/lint/new-rules.test.d.ts +9 -0
  41. package/dist/src/__tests__/lint/new-rules.test.js +402 -0
  42. package/dist/src/__tests__/lsp-rename.test.d.ts +8 -0
  43. package/dist/src/__tests__/lsp-rename.test.js +157 -0
  44. package/dist/src/__tests__/mc-integration/say-fstring.test.d.ts +11 -0
  45. package/dist/src/__tests__/mc-integration/say-fstring.test.js +220 -0
  46. package/dist/src/__tests__/mc-integration/stdlib-coverage-2.test.js +1 -1
  47. package/dist/src/__tests__/mc-integration/stdlib-coverage-3.test.js +1 -1
  48. package/dist/src/__tests__/mc-integration/stdlib-coverage-4.test.js +1 -1
  49. package/dist/src/__tests__/mc-integration/stdlib-coverage-5.test.js +1 -1
  50. package/dist/src/__tests__/mc-integration/stdlib-coverage-6.test.js +1 -1
  51. package/dist/src/__tests__/mc-integration/stdlib-coverage-7.test.js +1 -1
  52. package/dist/src/__tests__/mc-integration/stdlib-coverage-8.test.js +1 -1
  53. package/dist/src/__tests__/mc-syntax.test.js +4 -1
  54. package/dist/src/__tests__/monomorphize-coverage.test.d.ts +9 -0
  55. package/dist/src/__tests__/monomorphize-coverage.test.js +204 -0
  56. package/dist/src/__tests__/optimizer-cse.test.d.ts +7 -0
  57. package/dist/src/__tests__/optimizer-cse.test.js +226 -0
  58. package/dist/src/__tests__/parser.test.js +4 -13
  59. package/dist/src/__tests__/repl-server-extra.test.js +6 -7
  60. package/dist/src/__tests__/repl-server.test.js +5 -7
  61. package/dist/src/__tests__/stdlib/queue.test.js +6 -6
  62. package/dist/src/cli.js +0 -0
  63. package/dist/src/lexer/index.js +2 -1
  64. package/dist/src/lint/index.d.ts +12 -5
  65. package/dist/src/lint/index.js +730 -5
  66. package/dist/src/lsp/main.js +0 -0
  67. package/dist/src/mc-test/client.d.ts +21 -0
  68. package/dist/src/mc-test/client.js +34 -0
  69. package/dist/src/mir/lower.js +108 -6
  70. package/dist/src/optimizer/interprocedural.js +37 -2
  71. package/dist/src/parser/decl-parser.d.ts +19 -0
  72. package/dist/src/parser/decl-parser.js +323 -0
  73. package/dist/src/parser/expr-parser.d.ts +46 -0
  74. package/dist/src/parser/expr-parser.js +759 -0
  75. package/dist/src/parser/index.d.ts +8 -129
  76. package/dist/src/parser/index.js +13 -2262
  77. package/dist/src/parser/stmt-parser.d.ts +28 -0
  78. package/dist/src/parser/stmt-parser.js +577 -0
  79. package/dist/src/parser/type-parser.d.ts +20 -0
  80. package/dist/src/parser/type-parser.js +257 -0
  81. package/dist/src/parser/utils.d.ts +34 -0
  82. package/dist/src/parser/utils.js +141 -0
  83. package/docs/dev/README-mc-integration-tests.md +141 -0
  84. package/docs/lint-rules.md +162 -0
  85. package/docs/stdlib/bigint.md +2 -0
  86. package/editors/vscode/README.md +63 -41
  87. package/editors/vscode/out/extension.js +1881 -1776
  88. package/editors/vscode/out/lsp-server.js +4257 -3651
  89. package/editors/vscode/package-lock.json +3 -3
  90. package/editors/vscode/package.json +1 -1
  91. package/examples/loops-demo.mcrs +87 -0
  92. package/package.json +1 -1
  93. package/redscript-docs/docs/en/stdlib/advanced.md +629 -0
  94. package/redscript-docs/docs/en/stdlib/bigint.md +316 -0
  95. package/redscript-docs/docs/en/stdlib/bits.md +292 -0
  96. package/redscript-docs/docs/en/stdlib/bossbar.md +177 -0
  97. package/redscript-docs/docs/en/stdlib/calculus.md +289 -0
  98. package/redscript-docs/docs/en/stdlib/color.md +353 -0
  99. package/redscript-docs/docs/en/stdlib/combat.md +88 -0
  100. package/redscript-docs/docs/en/stdlib/cooldown.md +82 -0
  101. package/redscript-docs/docs/en/stdlib/dialog.md +155 -0
  102. package/redscript-docs/docs/en/stdlib/easing.md +558 -0
  103. package/redscript-docs/docs/en/stdlib/ecs.md +475 -0
  104. package/redscript-docs/docs/en/stdlib/effects.md +324 -0
  105. package/redscript-docs/docs/en/stdlib/events.md +3 -0
  106. package/redscript-docs/docs/en/stdlib/expr.md +45 -0
  107. package/redscript-docs/docs/en/stdlib/fft.md +141 -0
  108. package/redscript-docs/docs/en/stdlib/geometry.md +430 -0
  109. package/redscript-docs/docs/en/stdlib/graph.md +259 -0
  110. package/redscript-docs/docs/en/stdlib/heap.md +185 -0
  111. package/redscript-docs/docs/en/stdlib/interactions.md +179 -0
  112. package/redscript-docs/docs/en/stdlib/inventory.md +97 -0
  113. package/redscript-docs/docs/en/stdlib/linalg.md +557 -0
  114. package/redscript-docs/docs/en/stdlib/list.md +559 -0
  115. package/redscript-docs/docs/en/stdlib/map.md +140 -0
  116. package/redscript-docs/docs/en/stdlib/math.md +193 -0
  117. package/redscript-docs/docs/en/stdlib/math_hp.md +149 -0
  118. package/redscript-docs/docs/en/stdlib/matrix.md +403 -0
  119. package/redscript-docs/docs/en/stdlib/mobs.md +965 -0
  120. package/redscript-docs/docs/en/stdlib/noise.md +244 -0
  121. package/redscript-docs/docs/en/stdlib/ode.md +253 -0
  122. package/redscript-docs/docs/en/stdlib/parabola.md +342 -0
  123. package/redscript-docs/docs/en/stdlib/particles.md +311 -0
  124. package/redscript-docs/docs/en/stdlib/pathfind.md +255 -0
  125. package/redscript-docs/docs/en/stdlib/physics.md +493 -0
  126. package/redscript-docs/docs/en/stdlib/player.md +78 -0
  127. package/redscript-docs/docs/en/stdlib/quaternion.md +673 -0
  128. package/redscript-docs/docs/en/stdlib/queue.md +134 -0
  129. package/redscript-docs/docs/en/stdlib/random.md +223 -0
  130. package/redscript-docs/docs/en/stdlib/result.md +143 -0
  131. package/redscript-docs/docs/en/stdlib/scheduler.md +183 -0
  132. package/redscript-docs/docs/en/stdlib/set_int.md +190 -0
  133. package/redscript-docs/docs/en/stdlib/sets.md +101 -0
  134. package/redscript-docs/docs/en/stdlib/signal.md +400 -0
  135. package/redscript-docs/docs/en/stdlib/sort.md +104 -0
  136. package/redscript-docs/docs/en/stdlib/spawn.md +147 -0
  137. package/redscript-docs/docs/en/stdlib/state.md +142 -0
  138. package/redscript-docs/docs/en/stdlib/strings.md +154 -0
  139. package/redscript-docs/docs/en/stdlib/tags.md +3451 -0
  140. package/redscript-docs/docs/en/stdlib/teams.md +153 -0
  141. package/redscript-docs/docs/en/stdlib/timer.md +246 -0
  142. package/redscript-docs/docs/en/stdlib/vec.md +158 -0
  143. package/redscript-docs/docs/en/stdlib/world.md +298 -0
  144. package/redscript-docs/docs/zh/stdlib/advanced.md +615 -0
  145. package/redscript-docs/docs/zh/stdlib/bigint.md +316 -0
  146. package/redscript-docs/docs/zh/stdlib/bits.md +292 -0
  147. package/redscript-docs/docs/zh/stdlib/bossbar.md +170 -0
  148. package/redscript-docs/docs/zh/stdlib/calculus.md +287 -0
  149. package/redscript-docs/docs/zh/stdlib/color.md +353 -0
  150. package/redscript-docs/docs/zh/stdlib/combat.md +88 -0
  151. package/redscript-docs/docs/zh/stdlib/cooldown.md +84 -0
  152. package/redscript-docs/docs/zh/stdlib/dialog.md +152 -0
  153. package/redscript-docs/docs/zh/stdlib/easing.md +558 -0
  154. package/redscript-docs/docs/zh/stdlib/ecs.md +472 -0
  155. package/redscript-docs/docs/zh/stdlib/effects.md +324 -0
  156. package/redscript-docs/docs/zh/stdlib/events.md +3 -0
  157. package/redscript-docs/docs/zh/stdlib/expr.md +37 -0
  158. package/redscript-docs/docs/zh/stdlib/fft.md +128 -0
  159. package/redscript-docs/docs/zh/stdlib/geometry.md +430 -0
  160. package/redscript-docs/docs/zh/stdlib/graph.md +259 -0
  161. package/redscript-docs/docs/zh/stdlib/heap.md +185 -0
  162. package/redscript-docs/docs/zh/stdlib/interactions.md +160 -0
  163. package/redscript-docs/docs/zh/stdlib/inventory.md +94 -0
  164. package/redscript-docs/docs/zh/stdlib/linalg.md +543 -0
  165. package/redscript-docs/docs/zh/stdlib/list.md +561 -0
  166. package/redscript-docs/docs/zh/stdlib/map.md +132 -0
  167. package/redscript-docs/docs/zh/stdlib/math.md +193 -0
  168. package/redscript-docs/docs/zh/stdlib/math_hp.md +143 -0
  169. package/redscript-docs/docs/zh/stdlib/matrix.md +396 -0
  170. package/redscript-docs/docs/zh/stdlib/mobs.md +965 -0
  171. package/redscript-docs/docs/zh/stdlib/noise.md +244 -0
  172. package/redscript-docs/docs/zh/stdlib/ode.md +243 -0
  173. package/redscript-docs/docs/zh/stdlib/parabola.md +337 -0
  174. package/redscript-docs/docs/zh/stdlib/particles.md +307 -0
  175. package/redscript-docs/docs/zh/stdlib/pathfind.md +255 -0
  176. package/redscript-docs/docs/zh/stdlib/physics.md +493 -0
  177. package/redscript-docs/docs/zh/stdlib/player.md +78 -0
  178. package/redscript-docs/docs/zh/stdlib/quaternion.md +669 -0
  179. package/redscript-docs/docs/zh/stdlib/queue.md +124 -0
  180. package/redscript-docs/docs/zh/stdlib/random.md +222 -0
  181. package/redscript-docs/docs/zh/stdlib/result.md +147 -0
  182. package/redscript-docs/docs/zh/stdlib/scheduler.md +173 -0
  183. package/redscript-docs/docs/zh/stdlib/set_int.md +180 -0
  184. package/redscript-docs/docs/zh/stdlib/sets.md +107 -0
  185. package/redscript-docs/docs/zh/stdlib/signal.md +373 -0
  186. package/redscript-docs/docs/zh/stdlib/sort.md +104 -0
  187. package/redscript-docs/docs/zh/stdlib/spawn.md +142 -0
  188. package/redscript-docs/docs/zh/stdlib/state.md +134 -0
  189. package/redscript-docs/docs/zh/stdlib/strings.md +107 -0
  190. package/redscript-docs/docs/zh/stdlib/tags.md +3451 -0
  191. package/redscript-docs/docs/zh/stdlib/teams.md +150 -0
  192. package/redscript-docs/docs/zh/stdlib/timer.md +254 -0
  193. package/redscript-docs/docs/zh/stdlib/vec.md +158 -0
  194. package/redscript-docs/docs/zh/stdlib/world.md +289 -0
  195. package/src/__tests__/formatter-extra.test.ts +139 -0
  196. package/src/__tests__/global-vars.test.ts +171 -0
  197. package/src/__tests__/lint/new-rules.test.ts +437 -0
  198. package/src/__tests__/lsp-rename.test.ts +171 -0
  199. package/src/__tests__/mc-integration/say-fstring.test.ts +211 -0
  200. package/src/__tests__/mc-integration/stdlib-coverage-2.test.ts +1 -1
  201. package/src/__tests__/mc-integration/stdlib-coverage-3.test.ts +1 -1
  202. package/src/__tests__/mc-integration/stdlib-coverage-4.test.ts +1 -1
  203. package/src/__tests__/mc-integration/stdlib-coverage-5.test.ts +1 -1
  204. package/src/__tests__/mc-integration/stdlib-coverage-6.test.ts +1 -1
  205. package/src/__tests__/mc-integration/stdlib-coverage-7.test.ts +1 -1
  206. package/src/__tests__/mc-integration/stdlib-coverage-8.test.ts +1 -1
  207. package/src/__tests__/mc-syntax.test.ts +3 -0
  208. package/src/__tests__/monomorphize-coverage.test.ts +220 -0
  209. package/src/__tests__/optimizer-cse.test.ts +250 -0
  210. package/src/__tests__/parser.test.ts +4 -13
  211. package/src/__tests__/repl-server-extra.test.ts +6 -6
  212. package/src/__tests__/repl-server.test.ts +5 -6
  213. package/src/__tests__/stdlib/queue.test.ts +6 -6
  214. package/src/lexer/index.ts +2 -1
  215. package/src/lint/index.ts +713 -5
  216. package/src/mc-test/client.ts +40 -0
  217. package/src/mir/lower.ts +111 -2
  218. package/src/optimizer/interprocedural.ts +40 -2
  219. package/src/parser/decl-parser.ts +349 -0
  220. package/src/parser/expr-parser.ts +838 -0
  221. package/src/parser/index.ts +17 -2558
  222. package/src/parser/stmt-parser.ts +585 -0
  223. package/src/parser/type-parser.ts +276 -0
  224. package/src/parser/utils.ts +173 -0
  225. package/src/stdlib/queue.mcrs +19 -6
@@ -266,6 +266,46 @@ export class MCTestClient {
266
266
  }
267
267
  }
268
268
 
269
+ /**
270
+ * Dump all scoreboard entries for a namespace's objective (__<ns>).
271
+ * Returns { "$p0": 0, "$val": 11, "#result": 42, ... }
272
+ */
273
+ async dumpScores(ns: string): Promise<Record<string, number>> {
274
+ return this.get('/scoreboard/dump', { ns })
275
+ .then((r: any) => r.entries ?? {})
276
+ }
277
+
278
+ /**
279
+ * Dump all scoreboard entries for a specific objective.
280
+ */
281
+ async dumpScoresByObj(obj: string): Promise<Record<string, number>> {
282
+ return this.get('/scoreboard/dump', { obj })
283
+ .then((r: any) => r.entries ?? {})
284
+ }
285
+
286
+ /**
287
+ * Dump the raw NBT of an entire storage namespace.
288
+ */
289
+ async dumpStorage(storage: string): Promise<{ raw: string; ok: boolean }> {
290
+ return this.get('/storage/dump', { storage })
291
+ }
292
+
293
+ /**
294
+ * Assert multiple scoreboard values at once.
295
+ * Usage: await mc.assertScoreMap('ns', { '$p0': 11, '$ret': 2 })
296
+ */
297
+ async assertScoreMap(ns: string, expected: Record<string, number>): Promise<void> {
298
+ const actual = await this.dumpScores(ns)
299
+ for (const [key, val] of Object.entries(expected)) {
300
+ if (actual[key] !== val) {
301
+ throw new Error(
302
+ `assertScoreMap[${ns}] ${key}: expected ${val}, got ${actual[key] ?? '(unset)'}\n` +
303
+ `Full dump: ${JSON.stringify(actual)}`
304
+ )
305
+ }
306
+ }
307
+ }
308
+
269
309
  /**
270
310
  * Wait until a scoreboard value equals expected, up to timeout ms.
271
311
  */
package/src/mir/lower.ts CHANGED
@@ -136,9 +136,12 @@ export function lowerToMIR(hir: HIRModule, sourceFile?: string): MIRModule {
136
136
  else if (c.value.kind === 'float_lit') constValues.set(c.name, Math.round(c.value.value * 10000))
137
137
  }
138
138
 
139
+ // Collect module-level global variable names (mutable lets at top level)
140
+ const globalVarNames = new Set<string>(hir.globals.map(g => g.name))
141
+
139
142
  const allFunctions: MIRFunction[] = []
140
143
  for (const f of hir.functions) {
141
- const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter, undefined, hirFnMap, specializedFnsRegistry, undefined, enumPayloads, constValues, singletonStructs, displayImpls)
144
+ const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter, undefined, hirFnMap, specializedFnsRegistry, undefined, enumPayloads, constValues, singletonStructs, displayImpls, globalVarNames)
142
145
  allFunctions.push(fn, ...helpers)
143
146
  }
144
147
 
@@ -146,7 +149,7 @@ export function lowerToMIR(hir: HIRModule, sourceFile?: string): MIRModule {
146
149
  for (const ib of hir.implBlocks) {
147
150
  if (ib.traitName === 'Display') continue // Display impls are inlined, not emitted as functions
148
151
  for (const m of ib.methods) {
149
- const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter, enumPayloads, constValues)
152
+ const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, sourceFile, timerCounter, enumPayloads, constValues, globalVarNames)
150
153
  allFunctions.push(fn, ...helpers)
151
154
  }
152
155
  }
@@ -226,6 +229,8 @@ class FnContext {
226
229
  singletonStructs: Set<string> = new Set()
227
230
  /** Display trait impls: typeName → f-string parts from to_string body (inlined at call sites) */
228
231
  displayImpls: Map<string, HIRFStringPart[]> = new Map()
232
+ /** Module-level global variable names — reads/writes must go through scoreboard */
233
+ globalVarNames: Set<string> = new Set()
229
234
 
230
235
  constructor(
231
236
  namespace: string,
@@ -366,6 +371,7 @@ function lowerFunction(
366
371
  constValues: Map<string, number> = new Map(),
367
372
  singletonStructs: Set<string> = new Set(),
368
373
  displayImpls: Map<string, HIRFStringPart[]> = new Map(),
374
+ globalVarNames: Set<string> = new Set(),
369
375
  ): { fn: MIRFunction; helpers: MIRFunction[] } {
370
376
  const mirFnName = overrideName ?? fn.name
371
377
  const ctx = new FnContext(namespace, mirFnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter, enumPayloads)
@@ -373,6 +379,7 @@ function lowerFunction(
373
379
  ctx.constValues = constValues
374
380
  ctx.singletonStructs = singletonStructs
375
381
  ctx.displayImpls = displayImpls
382
+ ctx.globalVarNames = globalVarNames
376
383
  if (hirFnMap) ctx.hirFunctions = hirFnMap
377
384
  if (specializedFnsRegistry) ctx.specializedFnsRegistry = specializedFnsRegistry
378
385
  const fnMacroInfo = macroInfo.get(fn.name)
@@ -452,11 +459,13 @@ function lowerImplMethod(
452
459
  timerCounter: { count: number; timerId: number } = { count: 0, timerId: 0 },
453
460
  enumPayloads: Map<string, Map<string, { name: string; type: TypeNode }[]>> = new Map(),
454
461
  constValues: Map<string, number> = new Map(),
462
+ globalVarNames: Set<string> = new Set(),
455
463
  ): { fn: MIRFunction; helpers: MIRFunction[] } {
456
464
  const fnName = `${typeName}::${method.name}`
457
465
  const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo, enumDefs, timerCounter, enumPayloads)
458
466
  ctx.sourceFile = method.sourceFile ?? sourceFile
459
467
  ctx.constValues = constValues
468
+ ctx.globalVarNames = globalVarNames
460
469
  const fields = structDefs.get(typeName) ?? []
461
470
  const hasSelf = method.params.length > 0 && method.params[0].name === 'self'
462
471
 
@@ -1318,6 +1327,7 @@ function lowerStmt(
1318
1327
  const rawCmd = stmt.cmd
1319
1328
  .replace(/__NS__/g, ns)
1320
1329
  .replace(/__OBJ__/g, `__${ns}`)
1330
+ .replace(/__RS__/g, 'rs')
1321
1331
  ctx.emit({ kind: 'call', dst: null, fn: `__raw:${rawCmd}`, args: [] })
1322
1332
  break
1323
1333
  }
@@ -1520,6 +1530,13 @@ function lowerExpr(
1520
1530
  if (ctx.constValues.has(expr.name)) {
1521
1531
  return { kind: 'const', value: ctx.constValues.get(expr.name)! }
1522
1532
  }
1533
+ // Module-level global variable: read from scoreboard slot
1534
+ if (ctx.globalVarNames.has(expr.name)) {
1535
+ const t = ctx.freshTemp()
1536
+ ctx.emit({ kind: 'score_read', dst: t, player: expr.name, obj: `__${ctx.getNamespace()}` })
1537
+ scope.set(expr.name, t)
1538
+ return { kind: 'temp', name: t }
1539
+ }
1523
1540
  // Unresolved ident — could be a global or external reference
1524
1541
  const t = ctx.freshTemp()
1525
1542
  ctx.emit({ kind: 'copy', dst: t, src: { kind: 'const', value: 0 } })
@@ -1620,6 +1637,31 @@ function lowerExpr(
1620
1637
 
1621
1638
  case 'assign': {
1622
1639
  const val = lowerExpr(expr.value, ctx, scope)
1640
+ // Check if the target is a struct variable — if so, update its field temps
1641
+ // from the __rf_<field> return slots that the callee populated.
1642
+ const sv = ctx.structVars.get(expr.target)
1643
+ if (sv) {
1644
+ const fields = ctx.structDefs.get(sv.typeName) ?? []
1645
+ for (const fieldName of fields) {
1646
+ const existingFieldTemp = sv.fields.get(fieldName)
1647
+ // Reuse the existing field temp if it exists (so scoreboard slot stays stable),
1648
+ // otherwise allocate a fresh one.
1649
+ const fieldTemp = existingFieldTemp ?? ctx.freshTemp()
1650
+ ctx.emit({ kind: 'copy', dst: fieldTemp, src: { kind: 'temp', name: `__rf_${fieldName}` } })
1651
+ sv.fields.set(fieldName, fieldTemp)
1652
+ }
1653
+ return val
1654
+ }
1655
+ // Global variable assignment: write to scoreboard (score_write has side effects, DCE-safe)
1656
+ if (ctx.globalVarNames.has(expr.target)) {
1657
+ const globalObj = `__${ctx.getNamespace()}`
1658
+ ctx.emit({ kind: 'score_write', player: expr.target, obj: globalObj, src: val })
1659
+ // Also update scope temp so subsequent reads in this function see the new value
1660
+ const t = ctx.freshTemp()
1661
+ ctx.emit({ kind: 'score_read', dst: t, player: expr.target, obj: globalObj })
1662
+ scope.set(expr.target, t)
1663
+ return val
1664
+ }
1623
1665
  // Reuse the existing temp for this variable so that updates inside
1624
1666
  // if/while bodies are visible to outer code (we target mutable
1625
1667
  // scoreboard slots, not true SSA registers).
@@ -2074,6 +2116,72 @@ function lowerExpr(
2074
2116
 
2075
2117
  // Handle builtin calls → raw MC commands
2076
2118
  if (BUILTIN_SET.has(expr.fn)) {
2119
+ // Special case: say() with f_string → MC macro function ($say template)
2120
+ // MC `say` is plain text and cannot reference scoreboards directly;
2121
+ // use function macros (MC 1.20.2+) to interpolate variables.
2122
+ if (expr.fn === 'say' && expr.args[0]?.kind === 'f_string') {
2123
+ const fstr = precomputeFStringParts(expr.args[0], ctx, scope)
2124
+ if (fstr.kind === 'f_string') {
2125
+ const ns = ctx.getNamespace()
2126
+ const obj = `__${ns}`
2127
+ const helperName = `${ctx.getFnName()}__say_macro_${ctx.freshTemp()}`
2128
+
2129
+ // Build macro template: "text $(var) more text"
2130
+ let template = 'say '
2131
+ const macroVarNames: string[] = []
2132
+ for (const part of fstr.parts) {
2133
+ if (part.kind === 'text') {
2134
+ template += part.value
2135
+ } else {
2136
+ const inner = part.expr as HIRExpr
2137
+ if (inner.kind === 'ident') {
2138
+ // Strip leading $ from temp names for macro param names
2139
+ const varName = inner.name.startsWith('$') ? inner.name.slice(1) : inner.name
2140
+ template += `$(${varName})`
2141
+ macroVarNames.push(inner.name)
2142
+ } else if (inner.kind === 'int_lit') {
2143
+ template += String(inner.value)
2144
+ } else {
2145
+ template += '?'
2146
+ }
2147
+ }
2148
+ }
2149
+
2150
+ // Emit: copy each scoreboard var to rs:macro_args storage
2151
+ for (const varName of macroVarNames) {
2152
+ const cleanName = varName.startsWith('$') ? varName.slice(1) : varName
2153
+ ctx.emit({
2154
+ kind: 'call', dst: null,
2155
+ fn: `__raw:execute store result storage rs:macro_args ${cleanName} int 1 run scoreboard players get ${varName} ${obj}`,
2156
+ args: [],
2157
+ })
2158
+ }
2159
+
2160
+ // Build helper MIR function with isMacro: true
2161
+ const helperCtx = new FnContext(ns, helperName, ctx.structDefs, ctx.implMethods)
2162
+ helperCtx.emit({ kind: 'call', dst: null, fn: `__raw:$${template}`, args: [] })
2163
+ helperCtx.terminate({ kind: 'return', value: null })
2164
+ const helperReachable = computeReachable(helperCtx.blocks, 'entry')
2165
+ const helperBlocks = helperCtx.blocks.filter(b => helperReachable.has(b.id))
2166
+ computePreds(helperBlocks)
2167
+ ctx.helperFunctions.push({
2168
+ name: helperName,
2169
+ params: [],
2170
+ blocks: helperBlocks,
2171
+ entry: 'entry',
2172
+ isMacro: true,
2173
+ sourceSnippet: 'say macro helper',
2174
+ })
2175
+
2176
+ // Emit: function <helper> with storage rs:macro_args
2177
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:function ${ns}:${helperName} with storage rs:macro_args`, args: [] })
2178
+
2179
+ const t = ctx.freshTemp()
2180
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
2181
+ return { kind: 'temp', name: t }
2182
+ }
2183
+ }
2184
+
2077
2185
  // For text builtins with f-string args, precompute complex expressions to temp vars
2078
2186
  const TEXT_BUILTINS_SET = new Set(['tell', 'tellraw', 'title', 'subtitle', 'actionbar', 'announce'])
2079
2187
  let resolvedArgs = expr.args
@@ -2249,6 +2357,7 @@ function lowerExpr(
2249
2357
  ctx.constValues,
2250
2358
  ctx.singletonStructs,
2251
2359
  ctx.displayImpls,
2360
+ ctx.globalVarNames,
2252
2361
  )
2253
2362
  ctx.specializedFnsRegistry.set(specializedName, [specFn, ...specHelpers])
2254
2363
  }
@@ -58,7 +58,7 @@ function runOnePass(mod: MIRModule): MIRModule {
58
58
  if (fnMap.has(mangledName) || added.has(mangledName)) continue
59
59
 
60
60
  // Create specialized clone
61
- const specialized = specialize(callee, constArgs.map(a => a.value), mangledName)
61
+ const specialized = specialize(callee, constArgs.map(a => a.value), mangledName, mod.objective)
62
62
  newFunctions.push(specialized)
63
63
  added.add(mangledName)
64
64
  fnMap.set(mangledName, specialized)
@@ -89,7 +89,27 @@ function mangleName(name: string, args: number[]): string {
89
89
  return `${name}__const_${args.map(v => v < 0 ? `n${Math.abs(v)}` : String(v)).join('_')}`
90
90
  }
91
91
 
92
- function specialize(fn: MIRFunction, args: number[], newName: string): MIRFunction {
92
+ /**
93
+ * Returns true if the function has any __raw: calls that directly reference
94
+ * scoreboard param slots ($p0, $p1, ...) by name in the raw command string.
95
+ * Such functions use the raw() pattern to read params via scoreboard, so the
96
+ * specialized clone must pre-set those slots before executing the body.
97
+ */
98
+ function hasRawParamRefs(fn: MIRFunction, paramCount: number): boolean {
99
+ for (let i = 0; i < paramCount; i++) {
100
+ const pattern = `$p${i}`
101
+ for (const block of fn.blocks) {
102
+ for (const instr of block.instrs) {
103
+ if (instr.kind === 'call' && instr.fn.startsWith('__raw:') && instr.fn.includes(pattern)) {
104
+ return true
105
+ }
106
+ }
107
+ }
108
+ }
109
+ return false
110
+ }
111
+
112
+ function specialize(fn: MIRFunction, args: number[], newName: string, objective: string): MIRFunction {
93
113
  // Build substitution map: param.name → const operand
94
114
  const sub = new Map<Temp, Operand>()
95
115
  for (let i = 0; i < fn.params.length; i++) {
@@ -97,6 +117,24 @@ function specialize(fn: MIRFunction, args: number[], newName: string): MIRFuncti
97
117
  }
98
118
 
99
119
  const newBlocks = fn.blocks.map(block => substituteBlock(block, sub))
120
+
121
+ // If the function uses raw() commands that read from $p<i> scoreboard slots
122
+ // directly, we must pre-set those slots in the entry block so the raw commands
123
+ // see the correct values (the normal call convention sets $p<i> at the call
124
+ // site, but the specialized function is called with no args).
125
+ if (hasRawParamRefs(fn, args.length)) {
126
+ const entryBlock = newBlocks.find(b => b.id === fn.entry)
127
+ if (entryBlock) {
128
+ const scoreWrites: MIRInstr[] = args.map((value, i) => ({
129
+ kind: 'score_write' as const,
130
+ player: `$p${i}`,
131
+ obj: objective,
132
+ src: { kind: 'const' as const, value },
133
+ }))
134
+ entryBlock.instrs = [...scoreWrites, ...entryBlock.instrs]
135
+ }
136
+ }
137
+
100
138
  const specialized: MIRFunction = {
101
139
  ...fn,
102
140
  name: newName,
@@ -0,0 +1,349 @@
1
+ /**
2
+ * DeclParser — declaration parsing (fn/struct/enum/impl/interface/const/global/import).
3
+ * Extends StmtParser so declaration methods can call block/statement methods.
4
+ */
5
+
6
+ import type {
7
+ FnDecl, StructDecl, StructField, EnumDecl, EnumVariant, ImplBlock,
8
+ InterfaceDecl, InterfaceMethod, ConstDecl, GlobalDecl, Decorator,
9
+ TypeNode, Expr,
10
+ } from '../ast/types'
11
+ import { StmtParser } from './stmt-parser'
12
+
13
+ export class DeclParser extends StmtParser {
14
+ // -------------------------------------------------------------------------
15
+ // Struct
16
+ // -------------------------------------------------------------------------
17
+
18
+ parseStructDecl(): StructDecl {
19
+ const structToken = this.expect('struct')
20
+ const name = this.expect('ident').value
21
+ const extendsName = this.match('extends') ? this.expect('ident').value : undefined
22
+ this.expect('{')
23
+
24
+ const fields: StructField[] = []
25
+ while (!this.check('}') && !this.check('eof')) {
26
+ const fieldName = this.expect('ident').value
27
+ this.expect(':')
28
+ const fieldType = this.parseType()
29
+ fields.push({ name: fieldName, type: fieldType })
30
+ this.match(',')
31
+ }
32
+
33
+ this.expect('}')
34
+ return this.withLoc({ name, extends: extendsName, fields }, structToken)
35
+ }
36
+
37
+ // -------------------------------------------------------------------------
38
+ // Enum
39
+ // -------------------------------------------------------------------------
40
+
41
+ parseEnumDecl(): EnumDecl {
42
+ const enumToken = this.expect('enum')
43
+ const name = this.expect('ident').value
44
+ this.expect('{')
45
+
46
+ const variants: EnumVariant[] = []
47
+ let nextValue = 0
48
+
49
+ while (!this.check('}') && !this.check('eof')) {
50
+ const variantToken = this.expect('ident')
51
+ const variant: EnumVariant = { name: variantToken.value }
52
+
53
+ if (this.check('(')) {
54
+ this.advance()
55
+ const fields: { name: string; type: TypeNode }[] = []
56
+ while (!this.check(')') && !this.check('eof')) {
57
+ const fieldName = this.expect('ident').value
58
+ this.expect(':')
59
+ const fieldType = this.parseType()
60
+ fields.push({ name: fieldName, type: fieldType })
61
+ if (!this.match(',')) break
62
+ }
63
+ this.expect(')')
64
+ variant.fields = fields
65
+ }
66
+
67
+ if (this.match('=')) {
68
+ const valueToken = this.expect('int_lit')
69
+ variant.value = parseInt(valueToken.value, 10)
70
+ nextValue = variant.value + 1
71
+ } else {
72
+ variant.value = nextValue++
73
+ }
74
+
75
+ variants.push(variant)
76
+ if (!this.match(',')) break
77
+ }
78
+
79
+ this.expect('}')
80
+ return this.withLoc({ name, variants }, enumToken)
81
+ }
82
+
83
+ // -------------------------------------------------------------------------
84
+ // Impl Block
85
+ // -------------------------------------------------------------------------
86
+
87
+ parseImplBlock(): ImplBlock {
88
+ const implToken = this.expect('impl')
89
+ let traitName: string | undefined
90
+ let typeName: string
91
+ const firstName = this.expect('ident').value
92
+ if (this.match('for')) {
93
+ traitName = firstName
94
+ typeName = this.expect('ident').value
95
+ } else {
96
+ typeName = firstName
97
+ }
98
+ this.expect('{')
99
+
100
+ const methods: FnDecl[] = []
101
+ while (!this.check('}') && !this.check('eof')) {
102
+ methods.push(this.parseFnDecl(typeName))
103
+ }
104
+
105
+ this.expect('}')
106
+ return this.withLoc({ kind: 'impl_block', traitName, typeName, methods }, implToken)
107
+ }
108
+
109
+ // -------------------------------------------------------------------------
110
+ // Interface
111
+ // -------------------------------------------------------------------------
112
+
113
+ parseInterfaceDecl(): InterfaceDecl {
114
+ const ifaceToken = this.expect('interface')
115
+ const name = this.expect('ident').value
116
+ this.expect('{')
117
+
118
+ const methods: InterfaceMethod[] = []
119
+ while (!this.check('}') && !this.check('eof')) {
120
+ const fnToken = this.expect('fn')
121
+ const methodName = this.expect('ident').value
122
+ this.expect('(')
123
+ const params = this.parseInterfaceParams()
124
+ this.expect(')')
125
+ let returnType: TypeNode | undefined
126
+ if (this.match(':')) returnType = this.parseType()
127
+ methods.push(this.withLoc({ name: methodName, params, returnType }, fnToken) as InterfaceMethod)
128
+ }
129
+
130
+ this.expect('}')
131
+ return this.withLoc({ name, methods }, ifaceToken) as InterfaceDecl
132
+ }
133
+
134
+ // -------------------------------------------------------------------------
135
+ // Const / Global
136
+ // -------------------------------------------------------------------------
137
+
138
+ parseConstDecl(): ConstDecl {
139
+ const constToken = this.expect('const')
140
+ const name = this.expect('ident').value
141
+ let type: TypeNode | undefined
142
+ if (this.match(':')) type = this.parseType()
143
+ this.expect('=')
144
+ const value = this.parseLiteralExpr()
145
+ this.match(';')
146
+ const inferredType: TypeNode = type ?? (
147
+ value.kind === 'str_lit' ? { kind: 'named', name: 'string' } :
148
+ value.kind === 'bool_lit' ? { kind: 'named', name: 'bool' } :
149
+ value.kind === 'float_lit' ? { kind: 'named', name: 'fixed' } :
150
+ { kind: 'named', name: 'int' }
151
+ )
152
+ return this.withLoc({ name, type: inferredType, value }, constToken)
153
+ }
154
+
155
+ parseGlobalDecl(mutable: boolean): GlobalDecl {
156
+ const token = this.advance() // consume 'let'
157
+ const name = this.expect('ident').value
158
+ this.expect(':')
159
+ const type = this.parseType()
160
+ let init: Expr
161
+ if (this.match('=')) {
162
+ init = this.parseExpr()
163
+ } else {
164
+ init = { kind: 'int_lit', value: 0 }
165
+ }
166
+ this.match(';')
167
+ return this.withLoc({ kind: 'global', name, type, init, mutable }, token)
168
+ }
169
+
170
+ // -------------------------------------------------------------------------
171
+ // Function
172
+ // -------------------------------------------------------------------------
173
+
174
+ parseExportedFnDecl(): FnDecl {
175
+ this.expect('export')
176
+ const fn = this.parseFnDecl()
177
+ fn.isExported = true
178
+ return fn
179
+ }
180
+
181
+ parseFnDecl(implTypeName?: string): FnDecl {
182
+ const decorators = this.parseDecorators()
183
+ const watchObjective = decorators.find(decorator => decorator.name === 'watch')?.args?.objective
184
+
185
+ let isExported: boolean | undefined
186
+ const filteredDecorators = decorators.filter(d => {
187
+ if (d.name === 'keep') {
188
+ isExported = true
189
+ return false
190
+ }
191
+ return true
192
+ })
193
+
194
+ const fnToken = this.expect('fn')
195
+ const name = this.expect('ident').value
196
+
197
+ let typeParams: string[] | undefined
198
+ if (this.check('<')) {
199
+ this.advance()
200
+ typeParams = []
201
+ do {
202
+ typeParams.push(this.expect('ident').value)
203
+ } while (this.match(','))
204
+ this.expect('>')
205
+ }
206
+
207
+ this.expect('(')
208
+ const params = this.parseParams(implTypeName)
209
+ this.expect(')')
210
+
211
+ let returnType: TypeNode = { kind: 'named', name: 'void' }
212
+ if (this.match('->') || this.match(':')) {
213
+ returnType = this.parseType()
214
+ }
215
+
216
+ const body = this.parseBlock()
217
+ const closingBraceLine = this.tokens[this.pos - 1]?.line
218
+
219
+ const fn: FnDecl = this.withLoc(
220
+ { name, typeParams, params, returnType, decorators: filteredDecorators, body,
221
+ isLibraryFn: this.inLibraryMode || undefined, isExported, watchObjective },
222
+ fnToken,
223
+ )
224
+ if (fn.span && closingBraceLine) fn.span.endLine = closingBraceLine
225
+ return fn
226
+ }
227
+
228
+ parseDeclareStub(): void {
229
+ this.expect('fn')
230
+ this.expect('ident')
231
+ this.expect('(')
232
+ let depth = 1
233
+ while (!this.check('eof') && depth > 0) {
234
+ const t = this.advance()
235
+ if (t.kind === '(') depth++
236
+ else if (t.kind === ')') depth--
237
+ }
238
+ if (this.match(':') || this.match('->')) {
239
+ this.parseType()
240
+ }
241
+ this.match(';')
242
+ }
243
+
244
+ // -------------------------------------------------------------------------
245
+ // Decorators
246
+ // -------------------------------------------------------------------------
247
+
248
+ private parseDecorators(): Decorator[] {
249
+ const decorators: Decorator[] = []
250
+ while (this.check('decorator')) {
251
+ const token = this.advance()
252
+ const decorator = this.parseDecoratorValue(token.value)
253
+ decorators.push(decorator)
254
+ }
255
+ return decorators
256
+ }
257
+
258
+ parseDecoratorValue(value: string): Decorator {
259
+ const match = value.match(/^@([A-Za-z_][A-Za-z0-9_-]*)(?:\((.*)\))?$/s)
260
+ if (!match) {
261
+ this.error(`Invalid decorator: ${value}`)
262
+ }
263
+
264
+ const name = match[1] as Decorator['name']
265
+ const argsStr = match[2]
266
+
267
+ if (!argsStr) return { name }
268
+
269
+ if (name === 'profile' || name === 'benchmark' || name === 'memoize') {
270
+ this.error(`@${name} decorator does not accept arguments`)
271
+ }
272
+
273
+ const args: Decorator['args'] = {}
274
+
275
+ if (name === 'on') {
276
+ const eventTypeMatch = argsStr.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
277
+ if (eventTypeMatch) {
278
+ args.eventType = eventTypeMatch[1]
279
+ return { name, args }
280
+ }
281
+ }
282
+
283
+ if (name === 'watch' || name === 'on_trigger' || name === 'on_advancement' || name === 'on_craft' || name === 'on_join_team') {
284
+ const strMatch = argsStr.match(/^"([^"]*)"$/)
285
+ if (strMatch) {
286
+ if (name === 'watch') args.objective = strMatch[1]
287
+ else if (name === 'on_trigger') args.trigger = strMatch[1]
288
+ else if (name === 'on_advancement') args.advancement = strMatch[1]
289
+ else if (name === 'on_craft') args.item = strMatch[1]
290
+ else if (name === 'on_join_team') args.team = strMatch[1]
291
+ return { name, args }
292
+ }
293
+ }
294
+
295
+ if (name === 'config') {
296
+ const configMatch = argsStr.match(/^"([^"]+)"\s*,\s*default\s*:\s*(-?\d+(?:\.\d+)?)$/)
297
+ if (configMatch) {
298
+ return { name, args: { configKey: configMatch[1], configDefault: parseFloat(configMatch[2]) } }
299
+ }
300
+ const keyOnlyMatch = argsStr.match(/^"([^"]+)"$/)
301
+ if (keyOnlyMatch) {
302
+ return { name, args: { configKey: keyOnlyMatch[1] } }
303
+ }
304
+ this.error(`Invalid @config syntax. Expected: @config("key", default: value) or @config("key")`)
305
+ }
306
+
307
+ if (name === 'deprecated') {
308
+ const strMatch = argsStr.match(/^"([^"]*)"$/)
309
+ if (strMatch) return { name, args: { message: strMatch[1] } }
310
+ return { name, args: {} }
311
+ }
312
+
313
+ if (name === 'test') {
314
+ const strMatch = argsStr.match(/^"([^"]*)"$/)
315
+ if (strMatch) return { name, args: { testLabel: strMatch[1] } }
316
+ return { name, args: { testLabel: '' } }
317
+ }
318
+
319
+ if (name === 'require_on_load') {
320
+ const rawArgs: NonNullable<Decorator['rawArgs']> = []
321
+ for (const part of argsStr.split(',')) {
322
+ const trimmed = part.trim()
323
+ const identMatch = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)$/)
324
+ if (identMatch) {
325
+ rawArgs.push({ kind: 'string', value: identMatch[1] })
326
+ } else {
327
+ const strMatch = trimmed.match(/^"([^"]*)"$/)
328
+ if (strMatch) rawArgs.push({ kind: 'string', value: strMatch[1] })
329
+ }
330
+ }
331
+ return { name, rawArgs }
332
+ }
333
+
334
+ for (const part of argsStr.split(',')) {
335
+ const [key, val] = part.split('=').map(s => s.trim())
336
+ if (key === 'rate') args.rate = parseInt(val, 10)
337
+ else if (key === 'ticks') args.ticks = parseInt(val, 10)
338
+ else if (key === 'batch') args.batch = parseInt(val, 10)
339
+ else if (key === 'onDone') args.onDone = val.replace(/^["']|["']$/g, '')
340
+ else if (key === 'trigger') args.trigger = val
341
+ else if (key === 'advancement') args.advancement = val
342
+ else if (key === 'item') args.item = val
343
+ else if (key === 'team') args.team = val
344
+ else if (key === 'max') args.max = parseInt(val, 10)
345
+ }
346
+
347
+ return { name, args }
348
+ }
349
+ }