redscript-mc 1.2.30 → 2.1.0

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 (593) hide show
  1. package/.claudeignore +21 -0
  2. package/.github/workflows/ci.yml +1 -0
  3. package/README.md +12 -16
  4. package/README.zh.md +2 -2
  5. package/demo.gif +0 -0
  6. package/dist/cli.js +2 -554
  7. package/dist/compile.js +2 -266
  8. package/dist/index.js +2 -159
  9. package/dist/src/__tests__/budget.test.js +261 -0
  10. package/dist/src/__tests__/cli.test.js +104 -0
  11. package/dist/{__tests__ → src/__tests__}/dce.test.js +11 -47
  12. package/dist/{__tests__ → src/__tests__}/diagnostics.test.js +67 -40
  13. package/dist/src/__tests__/e2e/basic.test.d.ts +8 -0
  14. package/dist/src/__tests__/e2e/basic.test.js +140 -0
  15. package/dist/src/__tests__/e2e/coroutine.test.d.ts +7 -0
  16. package/dist/src/__tests__/e2e/coroutine.test.js +132 -0
  17. package/dist/src/__tests__/e2e/macros.test.d.ts +9 -0
  18. package/dist/src/__tests__/e2e/macros.test.js +182 -0
  19. package/dist/src/__tests__/e2e/migrate.test.d.ts +13 -0
  20. package/dist/src/__tests__/e2e/migrate.test.js +2739 -0
  21. package/dist/src/__tests__/e2e/stdlib-e2e.test.d.ts +10 -0
  22. package/dist/src/__tests__/e2e/stdlib-e2e.test.js +324 -0
  23. package/dist/src/__tests__/enum.test.d.ts +10 -0
  24. package/dist/src/__tests__/enum.test.js +389 -0
  25. package/dist/src/__tests__/generics.test.d.ts +14 -0
  26. package/dist/src/__tests__/generics.test.js +367 -0
  27. package/dist/src/__tests__/hir/desugar.test.js +234 -0
  28. package/dist/src/__tests__/incremental.test.d.ts +5 -0
  29. package/dist/src/__tests__/incremental.test.js +308 -0
  30. package/dist/src/__tests__/lir/lower.test.js +559 -0
  31. package/dist/src/__tests__/lir/types.test.js +185 -0
  32. package/dist/src/__tests__/lir/verify.test.js +221 -0
  33. package/dist/src/__tests__/lsp.test.d.ts +7 -0
  34. package/dist/src/__tests__/lsp.test.js +245 -0
  35. package/dist/{__tests__ → src/__tests__}/mc-integration.test.js +1 -3
  36. package/dist/src/__tests__/mc-version.test.d.ts +10 -0
  37. package/dist/src/__tests__/mc-version.test.js +154 -0
  38. package/dist/src/__tests__/mir/arithmetic.test.js +130 -0
  39. package/dist/src/__tests__/mir/control-flow.test.js +205 -0
  40. package/dist/src/__tests__/mir/verify.test.js +223 -0
  41. package/dist/src/__tests__/modules.test.d.ts +7 -0
  42. package/dist/src/__tests__/modules.test.js +333 -0
  43. package/dist/src/__tests__/optimizer/block_merge.test.js +78 -0
  44. package/dist/src/__tests__/optimizer/branch_simplify.test.js +58 -0
  45. package/dist/src/__tests__/optimizer/constant_fold.test.js +131 -0
  46. package/dist/src/__tests__/optimizer/copy_prop.test.js +91 -0
  47. package/dist/src/__tests__/optimizer/coroutine.test.d.ts +12 -0
  48. package/dist/src/__tests__/optimizer/coroutine.test.js +251 -0
  49. package/dist/src/__tests__/optimizer/dce.test.d.ts +1 -0
  50. package/dist/src/__tests__/optimizer/dce.test.js +76 -0
  51. package/dist/src/__tests__/optimizer/interprocedural.test.d.ts +1 -0
  52. package/dist/src/__tests__/optimizer/interprocedural.test.js +145 -0
  53. package/dist/src/__tests__/optimizer/lir/const_imm.test.d.ts +1 -0
  54. package/dist/src/__tests__/optimizer/lir/const_imm.test.js +138 -0
  55. package/dist/src/__tests__/optimizer/lir/dead_slot.test.d.ts +1 -0
  56. package/dist/src/__tests__/optimizer/lir/dead_slot.test.js +141 -0
  57. package/dist/src/__tests__/optimizer/lir/peephole.test.d.ts +1 -0
  58. package/dist/src/__tests__/optimizer/lir/peephole.test.js +126 -0
  59. package/dist/src/__tests__/optimizer/lir/pipeline.test.d.ts +1 -0
  60. package/dist/src/__tests__/optimizer/lir/pipeline.test.js +84 -0
  61. package/dist/src/__tests__/optimizer/nbt-batch.test.d.ts +1 -0
  62. package/dist/src/__tests__/optimizer/nbt-batch.test.js +110 -0
  63. package/dist/src/__tests__/optimizer/pipeline.test.d.ts +1 -0
  64. package/dist/src/__tests__/optimizer/pipeline.test.js +102 -0
  65. package/dist/src/__tests__/optimizer/selector-cache.test.d.ts +1 -0
  66. package/dist/src/__tests__/optimizer/selector-cache.test.js +103 -0
  67. package/dist/src/__tests__/optimizer/unroll.test.d.ts +1 -0
  68. package/dist/src/__tests__/optimizer/unroll.test.js +206 -0
  69. package/dist/src/__tests__/option.test.d.ts +14 -0
  70. package/dist/src/__tests__/option.test.js +275 -0
  71. package/dist/src/__tests__/parser.test.d.ts +1 -0
  72. package/dist/src/__tests__/repl.test.d.ts +1 -0
  73. package/dist/src/__tests__/schedule.test.d.ts +7 -0
  74. package/dist/src/__tests__/schedule.test.js +98 -0
  75. package/dist/src/__tests__/sourcemap.test.d.ts +7 -0
  76. package/dist/src/__tests__/sourcemap.test.js +227 -0
  77. package/dist/src/__tests__/tuple.test.d.ts +11 -0
  78. package/dist/src/__tests__/tuple.test.js +202 -0
  79. package/dist/src/__tests__/typechecker-strict.test.d.ts +10 -0
  80. package/dist/src/__tests__/typechecker-strict.test.js +197 -0
  81. package/dist/src/__tests__/typechecker.test.d.ts +1 -0
  82. package/dist/{ast → src/ast}/types.d.ts +58 -3
  83. package/dist/src/cache/deps.d.ts +41 -0
  84. package/dist/src/cache/deps.js +158 -0
  85. package/dist/src/cache/incremental.d.ts +35 -0
  86. package/dist/src/cache/incremental.js +165 -0
  87. package/dist/src/cache/index.d.ts +37 -0
  88. package/dist/src/cache/index.js +152 -0
  89. package/dist/{cli.d.ts → src/cli.d.ts} +1 -1
  90. package/dist/src/cli.js +474 -0
  91. package/dist/src/compile.d.ts +37 -0
  92. package/dist/src/compile.js +165 -0
  93. package/dist/{diagnostics → src/diagnostics}/index.d.ts +1 -1
  94. package/dist/{diagnostics → src/diagnostics}/index.js +8 -11
  95. package/dist/src/emit/compile.d.ts +29 -0
  96. package/dist/src/emit/compile.js +143 -0
  97. package/dist/src/emit/index.d.ts +26 -0
  98. package/dist/src/emit/index.js +223 -0
  99. package/dist/src/emit/modules.d.ts +29 -0
  100. package/dist/src/emit/modules.js +492 -0
  101. package/dist/src/emit/sourcemap.d.ts +53 -0
  102. package/dist/src/emit/sourcemap.js +73 -0
  103. package/dist/src/hir/lower.d.ts +15 -0
  104. package/dist/src/hir/lower.js +399 -0
  105. package/dist/src/hir/monomorphize.d.ts +22 -0
  106. package/dist/src/hir/monomorphize.js +379 -0
  107. package/dist/src/hir/types.d.ts +406 -0
  108. package/dist/src/hir/types.js +16 -0
  109. package/dist/src/index.d.ts +39 -0
  110. package/dist/src/index.js +67 -0
  111. package/dist/{lexer → src/lexer}/index.d.ts +1 -1
  112. package/dist/{lexer → src/lexer}/index.js +1 -0
  113. package/dist/src/lir/budget.d.ts +37 -0
  114. package/dist/src/lir/budget.js +280 -0
  115. package/dist/src/lir/lower.d.ts +15 -0
  116. package/dist/src/lir/lower.js +472 -0
  117. package/dist/src/lir/types.d.ts +139 -0
  118. package/dist/src/lir/types.js +11 -0
  119. package/dist/src/lir/verify.d.ts +14 -0
  120. package/dist/src/lir/verify.js +113 -0
  121. package/dist/src/lsp/main.d.ts +8 -0
  122. package/dist/src/lsp/main.js +11 -0
  123. package/dist/src/lsp/server.d.ts +11 -0
  124. package/dist/src/lsp/server.js +352 -0
  125. package/dist/{mc-test → src/mc-test}/runner.js +4 -3
  126. package/dist/src/mir/lower.d.ts +9 -0
  127. package/dist/src/mir/lower.js +1264 -0
  128. package/dist/src/mir/macro.d.ts +22 -0
  129. package/dist/src/mir/macro.js +168 -0
  130. package/dist/src/mir/types.d.ts +191 -0
  131. package/dist/src/mir/types.js +11 -0
  132. package/dist/src/mir/verify.d.ts +16 -0
  133. package/dist/src/mir/verify.js +216 -0
  134. package/dist/src/optimizer/block_merge.d.ts +12 -0
  135. package/dist/src/optimizer/block_merge.js +84 -0
  136. package/dist/src/optimizer/branch_simplify.d.ts +9 -0
  137. package/dist/src/optimizer/branch_simplify.js +28 -0
  138. package/dist/src/optimizer/constant_fold.d.ts +10 -0
  139. package/dist/src/optimizer/constant_fold.js +85 -0
  140. package/dist/src/optimizer/copy_prop.d.ts +9 -0
  141. package/dist/src/optimizer/copy_prop.js +113 -0
  142. package/dist/src/optimizer/coroutine.d.ts +34 -0
  143. package/dist/src/optimizer/coroutine.js +789 -0
  144. package/dist/src/optimizer/dce.d.ts +8 -0
  145. package/dist/src/optimizer/dce.js +156 -0
  146. package/dist/src/optimizer/interprocedural.d.ts +14 -0
  147. package/dist/src/optimizer/interprocedural.js +186 -0
  148. package/dist/src/optimizer/lir/const_imm.d.ts +12 -0
  149. package/dist/src/optimizer/lir/const_imm.js +139 -0
  150. package/dist/src/optimizer/lir/dead_slot.d.ts +14 -0
  151. package/dist/src/optimizer/lir/dead_slot.js +130 -0
  152. package/dist/src/optimizer/lir/peephole.d.ts +21 -0
  153. package/dist/src/optimizer/lir/peephole.js +52 -0
  154. package/dist/src/optimizer/lir/pipeline.d.ts +10 -0
  155. package/dist/src/optimizer/lir/pipeline.js +34 -0
  156. package/dist/src/optimizer/nbt-batch.d.ts +11 -0
  157. package/dist/src/optimizer/nbt-batch.js +51 -0
  158. package/dist/src/optimizer/pipeline.d.ts +14 -0
  159. package/dist/src/optimizer/pipeline.js +58 -0
  160. package/dist/src/optimizer/selector-cache.d.ts +22 -0
  161. package/dist/src/optimizer/selector-cache.js +100 -0
  162. package/dist/src/optimizer/unroll.d.ts +32 -0
  163. package/dist/src/optimizer/unroll.js +348 -0
  164. package/dist/{parser → src/parser}/index.d.ts +8 -0
  165. package/dist/{parser → src/parser}/index.js +204 -14
  166. package/dist/{repl.d.ts → src/repl.d.ts} +1 -1
  167. package/dist/{runtime → src/runtime}/index.js +1 -1
  168. package/dist/{typechecker → src/typechecker}/index.d.ts +4 -0
  169. package/dist/{typechecker → src/typechecker}/index.js +198 -13
  170. package/dist/src/types/mc-version.d.ts +24 -0
  171. package/dist/src/types/mc-version.js +49 -0
  172. package/docs/ROADMAP.md +395 -0
  173. package/docs/compiler-pipeline-redesign.md +2260 -0
  174. package/docs/optimization-ideas.md +1076 -0
  175. package/editors/vscode/out/extension.js +25176 -8000
  176. package/editors/vscode/package-lock.json +90 -6
  177. package/editors/vscode/package.json +3 -2
  178. package/editors/vscode/src/extension.ts +97 -67
  179. package/examples/showcase.mcrs +3 -3
  180. package/package.json +13 -6
  181. package/scripts/postbuild.js +15 -0
  182. package/src/__tests__/budget.test.ts +297 -0
  183. package/src/__tests__/cli.test.ts +8 -220
  184. package/src/__tests__/dce.test.ts +11 -56
  185. package/src/__tests__/diagnostics.test.ts +61 -41
  186. package/src/__tests__/e2e/basic.test.ts +154 -0
  187. package/src/__tests__/e2e/coroutine.test.ts +142 -0
  188. package/src/__tests__/e2e/macros.test.ts +199 -0
  189. package/src/__tests__/e2e/migrate.test.ts +3008 -0
  190. package/src/__tests__/e2e/stdlib-e2e.test.ts +348 -0
  191. package/src/__tests__/enum.test.ts +425 -0
  192. package/src/__tests__/generics.test.ts +390 -0
  193. package/src/__tests__/hir/desugar.test.ts +263 -0
  194. package/src/__tests__/incremental.test.ts +337 -0
  195. package/src/__tests__/lir/lower.test.ts +619 -0
  196. package/src/__tests__/lir/types.test.ts +207 -0
  197. package/src/__tests__/lir/verify.test.ts +249 -0
  198. package/src/__tests__/lsp.test.ts +270 -0
  199. package/src/__tests__/mc-integration.test.ts +1 -2
  200. package/src/__tests__/mc-version.test.ts +178 -0
  201. package/src/__tests__/mir/arithmetic.test.ts +156 -0
  202. package/src/__tests__/mir/control-flow.test.ts +242 -0
  203. package/src/__tests__/mir/verify.test.ts +254 -0
  204. package/src/__tests__/modules.test.ts +365 -0
  205. package/src/__tests__/optimizer/block_merge.test.ts +84 -0
  206. package/src/__tests__/optimizer/branch_simplify.test.ts +64 -0
  207. package/src/__tests__/optimizer/constant_fold.test.ts +145 -0
  208. package/src/__tests__/optimizer/copy_prop.test.ts +99 -0
  209. package/src/__tests__/optimizer/coroutine.test.ts +312 -0
  210. package/src/__tests__/optimizer/dce.test.ts +83 -0
  211. package/src/__tests__/optimizer/interprocedural.test.ts +174 -0
  212. package/src/__tests__/optimizer/lir/const_imm.test.ts +151 -0
  213. package/src/__tests__/optimizer/lir/dead_slot.test.ts +156 -0
  214. package/src/__tests__/optimizer/lir/peephole.test.ts +136 -0
  215. package/src/__tests__/optimizer/lir/pipeline.test.ts +113 -0
  216. package/src/__tests__/optimizer/nbt-batch.test.ts +119 -0
  217. package/src/__tests__/optimizer/pipeline.test.ts +116 -0
  218. package/src/__tests__/optimizer/selector-cache.test.ts +112 -0
  219. package/src/__tests__/optimizer/unroll.test.ts +231 -0
  220. package/src/__tests__/option.test.ts +299 -0
  221. package/src/__tests__/schedule.test.ts +105 -0
  222. package/src/__tests__/sourcemap.test.ts +254 -0
  223. package/src/__tests__/tuple.test.ts +220 -0
  224. package/src/__tests__/typechecker-strict.test.ts +216 -0
  225. package/src/ast/types.ts +39 -3
  226. package/src/cache/deps.ts +132 -0
  227. package/src/cache/incremental.ts +173 -0
  228. package/src/cache/index.ts +135 -0
  229. package/src/cli.ts +111 -195
  230. package/src/compile.ts +6 -162
  231. package/src/diagnostics/index.ts +8 -11
  232. package/src/emit/compile.ts +177 -0
  233. package/src/emit/index.ts +286 -0
  234. package/src/emit/modules.ts +581 -0
  235. package/src/emit/sourcemap.ts +101 -0
  236. package/src/hir/lower.ts +455 -0
  237. package/src/hir/monomorphize.ts +416 -0
  238. package/src/hir/types.ts +228 -0
  239. package/src/index.ts +37 -182
  240. package/src/lexer/index.ts +2 -1
  241. package/src/lir/budget.ts +321 -0
  242. package/src/lir/lower.ts +587 -0
  243. package/src/lir/types.ts +113 -0
  244. package/src/lir/verify.ts +129 -0
  245. package/src/lsp/main.ts +9 -0
  246. package/src/lsp/server.ts +414 -0
  247. package/src/mc-test/runner.ts +4 -3
  248. package/src/mir/lower.ts +1403 -0
  249. package/src/mir/macro.ts +167 -0
  250. package/src/mir/types.ts +117 -0
  251. package/src/mir/verify.ts +218 -0
  252. package/src/optimizer/block_merge.ts +93 -0
  253. package/src/optimizer/branch_simplify.ts +27 -0
  254. package/src/optimizer/constant_fold.ts +88 -0
  255. package/src/optimizer/copy_prop.ts +106 -0
  256. package/src/optimizer/coroutine.ts +996 -0
  257. package/src/optimizer/dce.ts +108 -653
  258. package/src/optimizer/interprocedural.ts +177 -0
  259. package/src/optimizer/lir/const_imm.ts +143 -0
  260. package/src/optimizer/lir/dead_slot.ts +123 -0
  261. package/src/optimizer/lir/peephole.ts +57 -0
  262. package/src/optimizer/lir/pipeline.ts +37 -0
  263. package/src/optimizer/nbt-batch.ts +50 -0
  264. package/src/optimizer/pipeline.ts +59 -0
  265. package/src/optimizer/selector-cache.ts +103 -0
  266. package/src/optimizer/unroll.ts +386 -0
  267. package/src/parser/index.ts +213 -16
  268. package/src/repl.ts +1 -1
  269. package/src/runtime/index.ts +1 -1
  270. package/src/stdlib/math.mcrs +4 -4
  271. package/src/templates/quest.mcrs +4 -4
  272. package/src/typechecker/index.ts +215 -15
  273. package/src/types/mc-version.ts +46 -0
  274. package/tsconfig.json +1 -1
  275. package/dist/__tests__/cli.test.js +0 -278
  276. package/dist/__tests__/codegen.test.js +0 -152
  277. package/dist/__tests__/e2e.test.d.ts +0 -6
  278. package/dist/__tests__/e2e.test.js +0 -1847
  279. package/dist/__tests__/entity-types.test.js +0 -203
  280. package/dist/__tests__/lowering.test.js +0 -1015
  281. package/dist/__tests__/macro.test.d.ts +0 -8
  282. package/dist/__tests__/macro.test.js +0 -305
  283. package/dist/__tests__/nbt.test.js +0 -82
  284. package/dist/__tests__/optimizer-advanced.test.js +0 -124
  285. package/dist/__tests__/optimizer.test.js +0 -149
  286. package/dist/__tests__/runtime.test.js +0 -289
  287. package/dist/__tests__/stdlib-advanced.test.d.ts +0 -4
  288. package/dist/__tests__/stdlib-advanced.test.js +0 -378
  289. package/dist/__tests__/stdlib-bigint.test.d.ts +0 -7
  290. package/dist/__tests__/stdlib-bigint.test.js +0 -428
  291. package/dist/__tests__/stdlib-math.test.d.ts +0 -7
  292. package/dist/__tests__/stdlib-math.test.js +0 -352
  293. package/dist/__tests__/stdlib-vec.test.d.ts +0 -4
  294. package/dist/__tests__/stdlib-vec.test.js +0 -264
  295. package/dist/__tests__/structure-optimizer.test.js +0 -33
  296. package/dist/__tests__/var-allocator.test.js +0 -69
  297. package/dist/codegen/cmdblock/index.d.ts +0 -26
  298. package/dist/codegen/cmdblock/index.js +0 -45
  299. package/dist/codegen/mcfunction/index.d.ts +0 -40
  300. package/dist/codegen/mcfunction/index.js +0 -606
  301. package/dist/codegen/structure/index.d.ts +0 -24
  302. package/dist/codegen/structure/index.js +0 -279
  303. package/dist/codegen/var-allocator.d.ts +0 -45
  304. package/dist/codegen/var-allocator.js +0 -104
  305. package/dist/compile.d.ts +0 -68
  306. package/dist/data/arena/function/__load.mcfunction +0 -6
  307. package/dist/data/arena/function/__tick.mcfunction +0 -2
  308. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +0 -3
  309. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +0 -1
  310. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +0 -3
  311. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +0 -7
  312. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +0 -1
  313. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +0 -4
  314. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +0 -6
  315. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +0 -1
  316. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +0 -4
  317. package/dist/data/arena/function/announce_leaders.mcfunction +0 -6
  318. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +0 -1
  319. package/dist/data/arena/function/arena_tick/then_0.mcfunction +0 -4
  320. package/dist/data/arena/function/arena_tick.mcfunction +0 -11
  321. package/dist/data/counter/function/__load.mcfunction +0 -5
  322. package/dist/data/counter/function/__tick.mcfunction +0 -2
  323. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +0 -1
  324. package/dist/data/counter/function/counter_tick/then_0.mcfunction +0 -3
  325. package/dist/data/counter/function/counter_tick.mcfunction +0 -11
  326. package/dist/data/gcd2/function/__load.mcfunction +0 -3
  327. package/dist/data/gcd2/function/abs/merge_2.mcfunction +0 -3
  328. package/dist/data/gcd2/function/abs/then_0.mcfunction +0 -5
  329. package/dist/data/gcd2/function/abs.mcfunction +0 -7
  330. package/dist/data/gcd2/function/gcd/loop_body_1.mcfunction +0 -7
  331. package/dist/data/gcd2/function/gcd/loop_check_0.mcfunction +0 -5
  332. package/dist/data/gcd2/function/gcd/loop_exit_2.mcfunction +0 -3
  333. package/dist/data/gcd2/function/gcd.mcfunction +0 -14
  334. package/dist/data/gcd3/function/__load.mcfunction +0 -3
  335. package/dist/data/gcd3/function/abs/merge_2.mcfunction +0 -3
  336. package/dist/data/gcd3/function/abs/then_0.mcfunction +0 -5
  337. package/dist/data/gcd3/function/abs.mcfunction +0 -7
  338. package/dist/data/gcd3/function/gcd/loop_body_1.mcfunction +0 -7
  339. package/dist/data/gcd3/function/gcd/loop_check_0.mcfunction +0 -5
  340. package/dist/data/gcd3/function/gcd/loop_exit_2.mcfunction +0 -3
  341. package/dist/data/gcd3/function/gcd.mcfunction +0 -14
  342. package/dist/data/gcd3/function/test.mcfunction +0 -7
  343. package/dist/data/gcd3nm/function/__load.mcfunction +0 -3
  344. package/dist/data/gcd3nm/function/abs/merge_2.mcfunction +0 -3
  345. package/dist/data/gcd3nm/function/abs/then_0.mcfunction +0 -5
  346. package/dist/data/gcd3nm/function/abs.mcfunction +0 -7
  347. package/dist/data/gcd3nm/function/gcd/loop_body_1.mcfunction +0 -7
  348. package/dist/data/gcd3nm/function/gcd/loop_check_0.mcfunction +0 -5
  349. package/dist/data/gcd3nm/function/gcd/loop_exit_2.mcfunction +0 -3
  350. package/dist/data/gcd3nm/function/gcd.mcfunction +0 -14
  351. package/dist/data/gcd3nm/function/test.mcfunction +0 -7
  352. package/dist/data/gcd_test/function/__load.mcfunction +0 -3
  353. package/dist/data/gcd_test/function/abs/merge_2.mcfunction +0 -3
  354. package/dist/data/gcd_test/function/abs/then_0.mcfunction +0 -5
  355. package/dist/data/gcd_test/function/abs.mcfunction +0 -7
  356. package/dist/data/gcd_test/function/gcd/loop_body_1.mcfunction +0 -7
  357. package/dist/data/gcd_test/function/gcd/loop_check_0.mcfunction +0 -5
  358. package/dist/data/gcd_test/function/gcd/loop_exit_2.mcfunction +0 -3
  359. package/dist/data/gcd_test/function/gcd.mcfunction +0 -14
  360. package/dist/data/isqrttest/function/__load.mcfunction +0 -6
  361. package/dist/data/isqrttest/function/isqrt/loop_body_4.mcfunction +0 -12
  362. package/dist/data/isqrttest/function/isqrt/loop_check_3.mcfunction +0 -5
  363. package/dist/data/isqrttest/function/isqrt/loop_exit_5.mcfunction +0 -3
  364. package/dist/data/isqrttest/function/isqrt/merge_2.mcfunction +0 -4
  365. package/dist/data/isqrttest/function/isqrt/merge_8.mcfunction +0 -6
  366. package/dist/data/isqrttest/function/isqrt/then_0.mcfunction +0 -3
  367. package/dist/data/isqrttest/function/isqrt/then_6.mcfunction +0 -3
  368. package/dist/data/isqrttest/function/isqrt.mcfunction +0 -7
  369. package/dist/data/isqrttest/function/test.mcfunction +0 -6
  370. package/dist/data/mathtest/function/__load.mcfunction +0 -3
  371. package/dist/data/mathtest/function/abs/merge_2.mcfunction +0 -3
  372. package/dist/data/mathtest/function/abs/then_0.mcfunction +0 -5
  373. package/dist/data/mathtest/function/abs.mcfunction +0 -6
  374. package/dist/data/mathtest/function/test.mcfunction +0 -5
  375. package/dist/data/minecraft/tags/function/load.json +0 -5
  376. package/dist/data/minecraft/tags/function/tick.json +0 -5
  377. package/dist/data/mypack/function/__load.mcfunction +0 -13
  378. package/dist/data/mypack/function/_atan_init.mcfunction +0 -2
  379. package/dist/data/mypack/function/abs/merge_2.mcfunction +0 -3
  380. package/dist/data/mypack/function/abs/then_0.mcfunction +0 -5
  381. package/dist/data/mypack/function/abs.mcfunction +0 -6
  382. package/dist/data/mypack/function/atan2_fixed/__sgi_1.mcfunction +0 -2
  383. package/dist/data/mypack/function/atan2_fixed/else_34.mcfunction +0 -3
  384. package/dist/data/mypack/function/atan2_fixed/loop_body_31.mcfunction +0 -19
  385. package/dist/data/mypack/function/atan2_fixed/loop_check_30.mcfunction +0 -5
  386. package/dist/data/mypack/function/atan2_fixed/loop_exit_32.mcfunction +0 -6
  387. package/dist/data/mypack/function/atan2_fixed/merge_11.mcfunction +0 -6
  388. package/dist/data/mypack/function/atan2_fixed/merge_14.mcfunction +0 -3
  389. package/dist/data/mypack/function/atan2_fixed/merge_17.mcfunction +0 -6
  390. package/dist/data/mypack/function/atan2_fixed/merge_2.mcfunction +0 -5
  391. package/dist/data/mypack/function/atan2_fixed/merge_20.mcfunction +0 -5
  392. package/dist/data/mypack/function/atan2_fixed/merge_23.mcfunction +0 -5
  393. package/dist/data/mypack/function/atan2_fixed/merge_26.mcfunction +0 -6
  394. package/dist/data/mypack/function/atan2_fixed/merge_29.mcfunction +0 -4
  395. package/dist/data/mypack/function/atan2_fixed/merge_38.mcfunction +0 -5
  396. package/dist/data/mypack/function/atan2_fixed/merge_41.mcfunction +0 -5
  397. package/dist/data/mypack/function/atan2_fixed/merge_44.mcfunction +0 -5
  398. package/dist/data/mypack/function/atan2_fixed/merge_47.mcfunction +0 -5
  399. package/dist/data/mypack/function/atan2_fixed/merge_5.mcfunction +0 -5
  400. package/dist/data/mypack/function/atan2_fixed/merge_8.mcfunction +0 -3
  401. package/dist/data/mypack/function/atan2_fixed/then_0.mcfunction +0 -5
  402. package/dist/data/mypack/function/atan2_fixed/then_12.mcfunction +0 -3
  403. package/dist/data/mypack/function/atan2_fixed/then_15.mcfunction +0 -5
  404. package/dist/data/mypack/function/atan2_fixed/then_18.mcfunction +0 -5
  405. package/dist/data/mypack/function/atan2_fixed/then_21.mcfunction +0 -3
  406. package/dist/data/mypack/function/atan2_fixed/then_24.mcfunction +0 -3
  407. package/dist/data/mypack/function/atan2_fixed/then_27.mcfunction +0 -6
  408. package/dist/data/mypack/function/atan2_fixed/then_3.mcfunction +0 -3
  409. package/dist/data/mypack/function/atan2_fixed/then_33.mcfunction +0 -5
  410. package/dist/data/mypack/function/atan2_fixed/then_36.mcfunction +0 -5
  411. package/dist/data/mypack/function/atan2_fixed/then_39.mcfunction +0 -5
  412. package/dist/data/mypack/function/atan2_fixed/then_42.mcfunction +0 -3
  413. package/dist/data/mypack/function/atan2_fixed/then_45.mcfunction +0 -5
  414. package/dist/data/mypack/function/atan2_fixed/then_6.mcfunction +0 -3
  415. package/dist/data/mypack/function/atan2_fixed/then_9.mcfunction +0 -5
  416. package/dist/data/mypack/function/atan2_fixed.mcfunction +0 -7
  417. package/dist/data/mypack/function/my_game.mcfunction +0 -10
  418. package/dist/data/quiz/function/__load.mcfunction +0 -16
  419. package/dist/data/quiz/function/__tick.mcfunction +0 -6
  420. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +0 -4
  421. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +0 -4
  422. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +0 -4
  423. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +0 -4
  424. package/dist/data/quiz/function/answer_a.mcfunction +0 -4
  425. package/dist/data/quiz/function/answer_b.mcfunction +0 -4
  426. package/dist/data/quiz/function/answer_c.mcfunction +0 -4
  427. package/dist/data/quiz/function/ask_question/else_1.mcfunction +0 -5
  428. package/dist/data/quiz/function/ask_question/else_4.mcfunction +0 -5
  429. package/dist/data/quiz/function/ask_question/else_7.mcfunction +0 -4
  430. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +0 -1
  431. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +0 -2
  432. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +0 -2
  433. package/dist/data/quiz/function/ask_question/then_0.mcfunction +0 -4
  434. package/dist/data/quiz/function/ask_question/then_3.mcfunction +0 -4
  435. package/dist/data/quiz/function/ask_question/then_6.mcfunction +0 -4
  436. package/dist/data/quiz/function/ask_question.mcfunction +0 -7
  437. package/dist/data/quiz/function/finish_quiz.mcfunction +0 -6
  438. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +0 -5
  439. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +0 -3
  440. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +0 -3
  441. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +0 -3
  442. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +0 -5
  443. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +0 -2
  444. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +0 -2
  445. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +0 -2
  446. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +0 -8
  447. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +0 -2
  448. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +0 -2
  449. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +0 -5
  450. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +0 -5
  451. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +0 -6
  452. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +0 -6
  453. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +0 -5
  454. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +0 -6
  455. package/dist/data/quiz/function/handle_answer.mcfunction +0 -11
  456. package/dist/data/quiz/function/start_quiz.mcfunction +0 -5
  457. package/dist/data/reqtest/function/__load.mcfunction +0 -4
  458. package/dist/data/reqtest/function/_table_init.mcfunction +0 -2
  459. package/dist/data/reqtest/function/no_trig.mcfunction +0 -3
  460. package/dist/data/reqtest/function/use_table.mcfunction +0 -4
  461. package/dist/data/reqtest2/function/__load.mcfunction +0 -3
  462. package/dist/data/reqtest2/function/no_trig.mcfunction +0 -3
  463. package/dist/data/runtime/function/__load.mcfunction +0 -5
  464. package/dist/data/runtime/function/__tick.mcfunction +0 -2
  465. package/dist/data/runtime/function/counter_tick/then_0.mcfunction +0 -3
  466. package/dist/data/runtime/function/counter_tick.mcfunction +0 -13
  467. package/dist/data/shop/function/__load.mcfunction +0 -7
  468. package/dist/data/shop/function/__tick.mcfunction +0 -3
  469. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +0 -4
  470. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +0 -5
  471. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +0 -5
  472. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +0 -3
  473. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +0 -2
  474. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +0 -2
  475. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +0 -2
  476. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +0 -4
  477. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +0 -4
  478. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +0 -4
  479. package/dist/data/shop/function/complete_purchase.mcfunction +0 -7
  480. package/dist/data/shop/function/handle_shop_trigger.mcfunction +0 -3
  481. package/dist/data/swap_test/function/__load.mcfunction +0 -3
  482. package/dist/data/swap_test/function/gcd_old/loop_body_1.mcfunction +0 -7
  483. package/dist/data/swap_test/function/gcd_old/loop_check_0.mcfunction +0 -5
  484. package/dist/data/swap_test/function/gcd_old/loop_exit_2.mcfunction +0 -3
  485. package/dist/data/swap_test/function/gcd_old.mcfunction +0 -8
  486. package/dist/data/turret/function/__load.mcfunction +0 -5
  487. package/dist/data/turret/function/__tick.mcfunction +0 -4
  488. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +0 -4
  489. package/dist/data/turret/function/deploy_turret.mcfunction +0 -8
  490. package/dist/data/turret/function/turret_tick/at_1.mcfunction +0 -2
  491. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +0 -2
  492. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +0 -2
  493. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +0 -3
  494. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +0 -1
  495. package/dist/data/turret/function/turret_tick.mcfunction +0 -5
  496. package/dist/gcd2.map.json +0 -15
  497. package/dist/gcd3.map.json +0 -17
  498. package/dist/gcd_test.map.json +0 -15
  499. package/dist/index.d.ts +0 -62
  500. package/dist/ir/builder.d.ts +0 -33
  501. package/dist/ir/builder.js +0 -99
  502. package/dist/ir/types.d.ts +0 -132
  503. package/dist/ir/types.js +0 -15
  504. package/dist/isqrttest.map.json +0 -15
  505. package/dist/lowering/index.d.ts +0 -188
  506. package/dist/lowering/index.js +0 -3403
  507. package/dist/mathtest.map.json +0 -6
  508. package/dist/mypack.map.json +0 -27
  509. package/dist/optimizer/commands.d.ts +0 -38
  510. package/dist/optimizer/commands.js +0 -451
  511. package/dist/optimizer/dce.d.ts +0 -34
  512. package/dist/optimizer/dce.js +0 -639
  513. package/dist/optimizer/passes.d.ts +0 -34
  514. package/dist/optimizer/passes.js +0 -243
  515. package/dist/optimizer/structure.d.ts +0 -9
  516. package/dist/optimizer/structure.js +0 -356
  517. package/dist/pack.mcmeta +0 -6
  518. package/dist/reqtest.map.json +0 -4
  519. package/dist/reqtest2.map.json +0 -4
  520. package/dist/runtime.map.json +0 -7
  521. package/dist/swap_test.map.json +0 -14
  522. package/src/__tests__/codegen.test.ts +0 -161
  523. package/src/__tests__/e2e.test.ts +0 -2039
  524. package/src/__tests__/entity-types.test.ts +0 -236
  525. package/src/__tests__/lowering.test.ts +0 -1185
  526. package/src/__tests__/macro.test.ts +0 -343
  527. package/src/__tests__/nbt.test.ts +0 -58
  528. package/src/__tests__/optimizer-advanced.test.ts +0 -144
  529. package/src/__tests__/optimizer.test.ts +0 -162
  530. package/src/__tests__/runtime.test.ts +0 -305
  531. package/src/__tests__/stdlib-advanced.test.ts +0 -379
  532. package/src/__tests__/stdlib-bigint.test.ts +0 -427
  533. package/src/__tests__/stdlib-math.test.ts +0 -374
  534. package/src/__tests__/stdlib-vec.test.ts +0 -259
  535. package/src/__tests__/structure-optimizer.test.ts +0 -38
  536. package/src/__tests__/var-allocator.test.ts +0 -75
  537. package/src/codegen/cmdblock/index.ts +0 -63
  538. package/src/codegen/mcfunction/index.ts +0 -662
  539. package/src/codegen/structure/index.ts +0 -346
  540. package/src/codegen/var-allocator.ts +0 -104
  541. package/src/ir/builder.ts +0 -116
  542. package/src/ir/types.ts +0 -134
  543. package/src/lowering/index.ts +0 -3876
  544. package/src/optimizer/commands.ts +0 -534
  545. package/src/optimizer/passes.ts +0 -250
  546. package/src/optimizer/structure.ts +0 -450
  547. /package/dist/{__tests__/cli.test.d.ts → src/__tests__/budget.test.d.ts} +0 -0
  548. /package/dist/{__tests__/codegen.test.d.ts → src/__tests__/cli.test.d.ts} +0 -0
  549. /package/dist/{__tests__ → src/__tests__}/compile-all.test.d.ts +0 -0
  550. /package/dist/{__tests__ → src/__tests__}/compile-all.test.js +0 -0
  551. /package/dist/{__tests__ → src/__tests__}/dce.test.d.ts +0 -0
  552. /package/dist/{__tests__ → src/__tests__}/diagnostics.test.d.ts +0 -0
  553. /package/dist/{__tests__ → src/__tests__}/formatter.test.d.ts +0 -0
  554. /package/dist/{__tests__ → src/__tests__}/formatter.test.js +0 -0
  555. /package/dist/{__tests__/entity-types.test.d.ts → src/__tests__/hir/desugar.test.d.ts} +0 -0
  556. /package/dist/{__tests__ → src/__tests__}/lexer.test.d.ts +0 -0
  557. /package/dist/{__tests__ → src/__tests__}/lexer.test.js +0 -0
  558. /package/dist/{__tests__/lowering.test.d.ts → src/__tests__/lir/lower.test.d.ts} +0 -0
  559. /package/dist/{__tests__/mc-syntax.test.d.ts → src/__tests__/lir/types.test.d.ts} +0 -0
  560. /package/dist/{__tests__/nbt.test.d.ts → src/__tests__/lir/verify.test.d.ts} +0 -0
  561. /package/dist/{__tests__ → src/__tests__}/mc-integration.test.d.ts +0 -0
  562. /package/dist/{__tests__/optimizer-advanced.test.d.ts → src/__tests__/mc-syntax.test.d.ts} +0 -0
  563. /package/dist/{__tests__ → src/__tests__}/mc-syntax.test.js +0 -0
  564. /package/dist/{__tests__/optimizer.test.d.ts → src/__tests__/mir/arithmetic.test.d.ts} +0 -0
  565. /package/dist/{__tests__/parser.test.d.ts → src/__tests__/mir/control-flow.test.d.ts} +0 -0
  566. /package/dist/{__tests__/repl.test.d.ts → src/__tests__/mir/verify.test.d.ts} +0 -0
  567. /package/dist/{__tests__/runtime.test.d.ts → src/__tests__/optimizer/block_merge.test.d.ts} +0 -0
  568. /package/dist/{__tests__/structure-optimizer.test.d.ts → src/__tests__/optimizer/branch_simplify.test.d.ts} +0 -0
  569. /package/dist/{__tests__/typechecker.test.d.ts → src/__tests__/optimizer/constant_fold.test.d.ts} +0 -0
  570. /package/dist/{__tests__/var-allocator.test.d.ts → src/__tests__/optimizer/copy_prop.test.d.ts} +0 -0
  571. /package/dist/{__tests__ → src/__tests__}/parser.test.js +0 -0
  572. /package/dist/{__tests__ → src/__tests__}/repl.test.js +0 -0
  573. /package/dist/{__tests__ → src/__tests__}/typechecker.test.js +0 -0
  574. /package/dist/{ast → src/ast}/types.js +0 -0
  575. /package/dist/{builtins → src/builtins}/metadata.d.ts +0 -0
  576. /package/dist/{builtins → src/builtins}/metadata.js +0 -0
  577. /package/dist/{events → src/events}/types.d.ts +0 -0
  578. /package/dist/{events → src/events}/types.js +0 -0
  579. /package/dist/{formatter → src/formatter}/index.d.ts +0 -0
  580. /package/dist/{formatter → src/formatter}/index.js +0 -0
  581. /package/dist/{mc-test → src/mc-test}/client.d.ts +0 -0
  582. /package/dist/{mc-test → src/mc-test}/client.js +0 -0
  583. /package/dist/{mc-test → src/mc-test}/runner.d.ts +0 -0
  584. /package/dist/{mc-test → src/mc-test}/setup.d.ts +0 -0
  585. /package/dist/{mc-test → src/mc-test}/setup.js +0 -0
  586. /package/dist/{mc-validator → src/mc-validator}/index.d.ts +0 -0
  587. /package/dist/{mc-validator → src/mc-validator}/index.js +0 -0
  588. /package/dist/{nbt → src/nbt}/index.d.ts +0 -0
  589. /package/dist/{nbt → src/nbt}/index.js +0 -0
  590. /package/dist/{repl.js → src/repl.js} +0 -0
  591. /package/dist/{runtime → src/runtime}/index.d.ts +0 -0
  592. /package/dist/{types → src/types}/entity-hierarchy.d.ts +0 -0
  593. /package/dist/{types → src/types}/entity-hierarchy.js +0 -0
@@ -0,0 +1,2260 @@
1
+ # RedScript Compiler Pipeline Redesign
2
+
3
+ > Status: **implemented** in `redscript-mc@2.0.0`
4
+ > Written: 2026-03-15 · Updated: 2026-03-15
5
+
6
+ ---
7
+
8
+ ## Motivation
9
+
10
+ The current compiler is a single-pass lowering that goes roughly
11
+ `Parser → AST → IR (2-address) → MCFunction`.
12
+ It works, but has accumulated tech debt that makes further optimizations fragile:
13
+
14
+ - IR is 2-address, which complicates use-def analysis and CSE
15
+ - "Optimization" and "lowering" are interleaved in the same pass
16
+ - Macro handling, builtin dispatch, and control-flow lowering all happen together
17
+ - Adding a new optimization often requires touching 3+ files
18
+
19
+ The plan below separates concerns cleanly into 7 stages.
20
+
21
+ ---
22
+
23
+ ## Stage 1 — Frontend: Parser → AST
24
+
25
+ **Responsibilities:** source text → well-formed typed syntax tree
26
+
27
+ - Lexing / parsing (already solid)
28
+ - Name resolution (resolve identifiers to their declarations)
29
+ - Type checking (infer and check all expression types)
30
+ - Scope analysis (closures, shadowing, struct fields)
31
+
32
+ **Output:** a fully type-annotated AST where every node carries its type.
33
+
34
+ No desugaring here. Keep AST faithful to source.
35
+
36
+ ---
37
+
38
+ ## Stage 2 — AST → HIR *(High-level IR)*
39
+
40
+ **Goal:** eliminate syntax sugar; keep structured control flow.
41
+
42
+ Transforms applied:
43
+
44
+ | Source construct | HIR form |
45
+ |---|---|
46
+ | `for (init; cond; step)` | `while` loop with explicit init/step |
47
+ | `a += b` | `a = a + b` (or dedicated `Op` node) |
48
+ | `let x = complex_expr` | declaration + separate assignment |
49
+ | `a && b` | `if a { b } else { false }` |
50
+ | `a \|\| b` | `if a { true } else { b }` |
51
+ | `cond ? a : b` | `if cond { a } else { b }` |
52
+ | comma expressions | sequential statements |
53
+ | `foreach` | explicit iterator variable + while |
54
+
55
+ HIR is still **structured** (no gotos, no basic blocks yet).
56
+ All types are known; all names are resolved.
57
+
58
+ ---
59
+
60
+ ## Stage 3 — HIR → MIR *(Mid-level IR, 3-address CFG)*
61
+
62
+ **Goal:** structured control flow → explicit Control Flow Graph (CFG).
63
+
64
+ - Introduce basic blocks with explicit predecessors/successors
65
+ - Introduce unlimited fresh temporaries
66
+ - **3-address form**: every instruction has at most one operation
67
+
68
+ ```
69
+ # MIR example
70
+ t1 = add a, b
71
+ t2 = mul t1, c
72
+ x = mov t2
73
+ ```
74
+
75
+ Why 3-address (not 2-address like the current IR)?
76
+
77
+ - Use-def chains are trivial to build
78
+ - CSE: identical RHS expressions are immediately comparable
79
+ - Constant propagation: single definition per temp makes dataflow simple
80
+ - Expression reordering: no aliasing through the destination
81
+
82
+ Control flow:
83
+ - `if` → conditional branch + merge block
84
+ - `while` → loop header + body + exit
85
+ - `return` → explicit jump to exit block
86
+ - `break`/`continue` → explicit jumps
87
+
88
+ ---
89
+
90
+ ## Stage 4 — MIR Optimization Passes
91
+
92
+ Run on the 3-address CFG. Passes are composable and independent.
93
+
94
+ ### Required (correctness + baseline perf)
95
+
96
+ | Pass | Description |
97
+ |---|---|
98
+ | **Constant folding** | `t = add 3, 4` → `t = 7` |
99
+ | **Constant propagation** | replace uses of single-def consts with the value |
100
+ | **Copy propagation** | `x = mov y; ... use x` → `... use y` |
101
+ | **Dead code elimination** | remove defs with no live uses |
102
+ | **Unreachable block elimination** | remove blocks with no predecessors |
103
+ | **Block merging** | merge unconditional-jump-only block chains |
104
+ | **Branch simplification** | `if true` / `if false` → unconditional jump |
105
+
106
+ ### High value (significantly smaller output)
107
+
108
+ | Pass | Description |
109
+ |---|---|
110
+ | **Liveness analysis** | compute live sets for all blocks (required by DCE + alloc) |
111
+ | **Temp coalescing** | merge non-interfering temporaries (reduces slots) |
112
+ | **Destination forwarding** | `t = op a, b; x = mov t` → `x = op a, b` when t dead |
113
+ | **Local CSE** | eliminate repeated identical subexpressions within a block |
114
+ | **Small function inlining** | inline trivial callee bodies at call site |
115
+
116
+ ---
117
+
118
+ ## Stage 5 — MIR → LIR *(Low-level IR, Minecraft-friendly)*
119
+
120
+ **Goal:** abstract operations → Minecraft scoreboard semantics.
121
+
122
+ This is where 3-address gets translated to 2-address **with awareness of
123
+ the destination-reuse pattern** that MC scoreboard requires.
124
+
125
+ ```
126
+ # MIR (3-address)
127
+ t1 = add a, b
128
+ t2 = add t1, c
129
+ x = mov t2
130
+
131
+ # LIR output (when t1, t2 not live after)
132
+ ScoreCopy x, a
133
+ ScoreAdd x, b
134
+ ScoreAdd x, c
135
+ ```
136
+
137
+ Key decisions made here:
138
+
139
+ - Which values live in scoreboards vs NBT storage
140
+ - How to represent `execute`-chained subcommands
141
+ - Macro parameter injection points
142
+ - `$(param)` substitution for dynamic coordinates
143
+
144
+ This stage should be *target-specific* but not yet emitting strings.
145
+ LIR instructions are typed MC operations, not raw text.
146
+
147
+ ---
148
+
149
+ ## Stage 6 — LIR Optimization
150
+
151
+ Backend-specific optimizations that only make sense post-lowering:
152
+
153
+ | Pass | Description |
154
+ |---|---|
155
+ | **Scoreboard slot allocation** | minimize number of distinct objective slots used |
156
+ | **`execute` context extraction** | hoist repeated `execute as @p at @s` prefixes |
157
+ | **`execute` chain merging** | `execute A run execute B run cmd` → `execute A B run cmd` |
158
+ | **Guard block merging** | merge adjacent `execute if score ... matches` guards |
159
+ | **NBT/score carrier selection** | decide when to spill to storage vs keep in score |
160
+ | **Peephole** | local pattern rewrites (e.g. `op X = X` → nop) |
161
+ | **Command deduplication** | remove identical adjacent commands |
162
+ | **Function inlining / outlining** | inline trivial functions; outline repeated sequences |
163
+ | **Block layout** | order blocks to minimize `function` call overhead |
164
+
165
+ ---
166
+
167
+ ## Stage 7 — Emission
168
+
169
+ **Goal:** LIR → `.mcfunction` files on disk.
170
+
171
+ - Assign each LIR block to a `namespace:path/block_name` function
172
+ - Emit `function` calls for control flow edges
173
+ - Generate `load.mcfunction`: objective creation, storage init, const table
174
+ - Generate `tick.mcfunction`: `@tick`-annotated function dispatch
175
+ - Emit call graph in dependency order
176
+ - Write sourcemap (IR name → file:line for diagnostics)
177
+
178
+ ---
179
+
180
+ ## Summary
181
+
182
+ ```
183
+ Source
184
+
185
+ ▼ Stage 1
186
+ AST (typed, name-resolved)
187
+
188
+ ▼ Stage 2
189
+ HIR (desugared, structured)
190
+
191
+ ▼ Stage 3
192
+ MIR (3-address CFG)
193
+
194
+ ▼ Stage 4
195
+ MIR' (optimized)
196
+
197
+ ▼ Stage 5
198
+ LIR (MC-friendly 2-address)
199
+
200
+ ▼ Stage 6
201
+ LIR' (backend-optimized)
202
+
203
+ ▼ Stage 7
204
+ .mcfunction files
205
+ ```
206
+
207
+ The key insight: **optimization-friendly representation (Stage 4) and
208
+ target-friendly representation (Stage 6) are separate**. Trying to do
209
+ both at once is why the current IR is hard to extend.
210
+
211
+ ---
212
+
213
+ ## Migration Notes
214
+
215
+ - Current `src/lowering/index.ts` = Stage 2 + 3 + 5 merged → split into three
216
+ - Current `src/optimizer/` = partial Stage 4, operating on 2-address → rewrite around 3-address MIR
217
+ - Current `src/codegen/` = Stage 6 + 7 merged → split at the LIR boundary
218
+ - Current `src/ir/` = needs 3-address extension or replacement
219
+ - Tests: keep end-to-end `.mcrs → .mcfunction` tests as regression suite; add unit tests per stage
220
+
221
+ ---
222
+
223
+ *This document was drafted to guide the next major refactor. Details may change during implementation.*
224
+
225
+ ---
226
+
227
+ ## Tech Stack & Infrastructure Decisions
228
+
229
+ ### Language: stay in TypeScript
230
+
231
+ The MC target is too domain-specific for a general backend (LLVM, Cranelift, QBE)
232
+ to add value. The compilation workload is also small enough (functions are
233
+ typically < 100 MIR instructions) that performance is not a concern.
234
+ TS gives good type safety for IR node types and is already the existing codebase.
235
+
236
+ ### SSA: no, use versioned temporaries
237
+
238
+ | | SSA | Versioned temps |
239
+ |---|---|---|
240
+ | Constant prop | trivial one-pass | fixed-point iteration needed |
241
+ | DCE | trivial | single backward sweep |
242
+ | Copy prop | trivial | one extra level of indirection |
243
+ | Construction | dominator tree + φ-insertion | trivial, just increment a counter |
244
+ | Deconstruction | must run before LIR | N/A |
245
+
246
+ For function bodies in the 20–200 instruction range with no complex loop
247
+ induction variable analysis, versioned temps are sufficient. SSA complexity
248
+ is not justified at this scale.
249
+
250
+ ### Pass framework: nanopass style
251
+
252
+ Each optimization pass should be a pure function:
253
+
254
+ ```typescript
255
+ type Pass = (module: MIRModule) => MIRModule
256
+ ```
257
+
258
+ - No mutation of shared global state
259
+ - Can be verified with `verifyMIR(module)` between passes
260
+ - Pipeline is just an array of passes: `const pipeline: Pass[] = [constantFold, copyProp, dce, ...]`
261
+ - Easy to toggle a pass for debugging
262
+ - Easy to add `before/after` IR dumps per pass
263
+
264
+ Current code mixes optimization and lowering in the same methods.
265
+ The nanopass shape forces separation.
266
+
267
+ ### What to reuse from the current codebase
268
+
269
+ | Module | Verdict | Notes |
270
+ |---|---|---|
271
+ | `src/parser/` | **keep as-is** | solid, already produces typed AST |
272
+ | `src/lexer/` | **keep as-is** | — |
273
+ | `src/runtime/` | **keep as-is** | MC runtime simulator used in tests |
274
+ | `src/__tests__/` | **keep e2e tests** | regression suite covering `.mcrs → .mcfunction` |
275
+ | `src/optimizer/passes.ts` | **port logic, rewrite impl** | copy the *idea*, not the regex machinery |
276
+ | `src/optimizer/commands.ts` | **discard** | regex-based command matching, replace with typed LIR |
277
+ | `src/ir/` | **replace** | extend with 3-address form and explicit CFG |
278
+ | `src/lowering/` | **split into Stage 2+3+5** | 3,500-line file doing too many things |
279
+ | `src/codegen/` | **split into Stage 6+7** | keep emission logic, rebuild on new LIR |
280
+
281
+ ---
282
+
283
+ ## Current Architecture (as of v1.2.x)
284
+
285
+ Knowing what we have makes migration planning concrete.
286
+
287
+ ```
288
+ src/
289
+ lexer/index.ts Tokenizer
290
+ parser/index.ts Recursive-descent parser → AST
291
+ lowering/index.ts AST → IR (3,500 lines; Stages 2+3+5 merged)
292
+ ir/index.ts IR types: IRModule, IRFunction, IRBlock, IRInstr
293
+ optimizer/
294
+ passes.ts Optimization passes on 2-addr IR
295
+ commands.ts Regex-based command analysis (OBJ pattern etc.)
296
+ structure.ts Structural analysis helpers
297
+ codegen/
298
+ mcfunction/index.ts IR → .mcfunction text files
299
+ compile.ts Top-level compile() entry point
300
+ cli.ts CLI wrapper
301
+ runtime/index.ts MCRuntime: scoreboard + storage simulator for tests
302
+ stdlib/
303
+ math.mcrs sin/cos/sqrt tables + trig, 91-entry lookup
304
+ vec.mcrs 2D/3D vector ops using fixed-point
305
+ advanced.mcrs smoothstep, smootherstep, clamp, etc.
306
+ bigint.mcrs 8-limb base-10000 BigInt
307
+ timer.mcrs single-instance Timer (tick countdown)
308
+ ```
309
+
310
+ The IR is 2-address:
311
+
312
+ ```
313
+ x = a (copy)
314
+ x += b (in-place add)
315
+ x *= c (in-place mul)
316
+ ```
317
+
318
+ Arithmetic sequences are modeled as chains of in-place updates on a single
319
+ destination, which obscures the value-dependency graph and complicates CSE.
320
+
321
+ Optimization passes operate on `IRInstr` objects that contain raw MC command
322
+ strings. Several passes (copy propagation, CSE, block merge) parse those strings
323
+ with regular expressions to extract slot names and objective names, which is
324
+ fragile and tightly coupled to the objective naming scheme.
325
+
326
+ ---
327
+
328
+ ## Lessons Learned / Design Pitfalls
329
+
330
+ These are real bugs or design limitations that shaped the current codebase.
331
+ The redesign should address all of them.
332
+
333
+ ### 1. Global mutable objective name state
334
+
335
+ **Problem:** The scoreboard objective name (`rs`, then `__namespace`) is stored
336
+ in a module-level mutable variable. To support multiple datapacks in one process,
337
+ we had to add `setScoreboardObjective()`, `setOptimizerObjective()`,
338
+ `setStructureObjective()` — three separate setters across three files.
339
+ The optimizer's regex patterns also had to be regenerated dynamically.
340
+
341
+ **Root cause:** objective name was a constant baked into every pass.
342
+
343
+ **Fix in redesign:** Pass a `CompileContext` record through the entire pipeline.
344
+ No global state.
345
+
346
+ ---
347
+
348
+ ### 2. Optimizer regex matching on command strings
349
+
350
+ **Problem:** Copy propagation, CSE, and block merge all pattern-match on raw
351
+ MC command text like `"scoreboard players operation $x rs = $y rs"`.
352
+ When the objective name changed from `rs` to `__namespace`, every regex had
353
+ to be updated. When the regex didn't account for a case (e.g. `$x rs 0`),
354
+ the pass silently failed.
355
+
356
+ **Root cause:** IR instructions are strings, not typed nodes.
357
+
358
+ **Fix in redesign:** LIR instructions are typed (e.g. `ScoreCopy`, `ScoreAdd`).
359
+ Passes pattern-match on structured nodes, not strings.
360
+
361
+ ---
362
+
363
+ ### 3. Lowering, desugaring, and macro detection all in one pass
364
+
365
+ **Problem:** `src/lowering/index.ts` is 3,500 lines that simultaneously:
366
+ - Desugar `for`/`+=`/ternary
367
+ - Build basic blocks and terminators
368
+ - Handle builtin dispatch (particle, setblock, tp, ...)
369
+ - Detect macro parameters and rewrite coordinates as `$(param)`
370
+ - Manage function specialization for stdlib callbacks
371
+ - Track struct fields and impl method dispatch
372
+
373
+ Adding any new feature requires understanding all of this at once.
374
+
375
+ **Fix in redesign:** Each of these is a separate stage.
376
+
377
+ ---
378
+
379
+ ### 4. `\x01` sentinel for macro line prefix
380
+
381
+ **Problem:** When a builtin command needed a `$` prefix for MC macro syntax
382
+ (e.g. `$particle ... ^$(px) ...`), the lowering used a literal `$` prefix.
383
+ The codegen's `resolveRaw()` then saw `$particle` and allocated a fresh
384
+ temporary named `particle`, replacing the `$` prefix with `$v` (or whatever
385
+ the temp was allocated as). The particle command silently became `$v minecraft:end_rod ...`
386
+ which MC ignored.
387
+
388
+ **Root cause:** The `$var` variable reference syntax and the MC macro `$` line
389
+ prefix shared the same sigil in raw command strings.
390
+
391
+ **Fix applied:** Use `\x01` as sentinel in IR; codegen converts `\x01` → `$`
392
+ after variable resolution.
393
+
394
+ **Fix in redesign:** Typed LIR instruction `MacroParticle { ... }` — no raw
395
+ string parsing needed.
396
+
397
+ ---
398
+
399
+ ### 5. Cross-function variable name collision
400
+
401
+ **Problem:** Two functions `foo` and `bar` could each declare a variable `x`,
402
+ both getting lowered to scoreboard slot `$x`. If both were inlined or called
403
+ in the same tick context, they shared the slot.
404
+
405
+ **Fix applied:** IR variable names are scoped as `$fnname_varname`.
406
+
407
+ **Fix in redesign:** 3-address MIR uses globally-unique temporaries (counter-based).
408
+ Slot allocation is a separate explicit pass.
409
+
410
+ ---
411
+
412
+ ### 6. `mc_name` early-return bypassed `#rs` resolution
413
+
414
+ **Problem:** In `exprToScoreboardObjective`, the handler for `mc_name` returned
415
+ `expr.value` directly, bypassing the `#rs → LOWERING_OBJ` special case.
416
+ All timer stdlib tests that used `#rs` as the objective were matching the
417
+ literal string `"rs"` instead of the namespace-specific objective.
418
+
419
+ **Root cause:** Early-return before the special-case check.
420
+
421
+ **Fix applied:** Check `value === 'rs'` before the early return.
422
+
423
+ **Fix in redesign:** Objective references should be a first-class IR type,
424
+ not a string that might be the literal `"rs"` or the special token `"rs"`.
425
+
426
+ ---
427
+
428
+ ### 7. Timer is single-instance
429
+
430
+ **Problem:** `timer.mcrs` stores tick count and active state on fake players
431
+ `timer_ticks` and `timer_active`. All Timer instances share the same player,
432
+ so only one Timer can be active at a time.
433
+
434
+ **Root cause:** No per-instance storage mechanism. The `_id` field was stubbed
435
+ but never implemented.
436
+
437
+ **Path forward:** Per-instance state needs either:
438
+ - Named fake player per instance: `timer_1_ticks`, `timer_2_ticks`, ... (requires macro `$-prefixed scoreboard` commands)
439
+ - NBT array slot per instance (same pattern as BigInt limbs)
440
+
441
+ ---
442
+
443
+ ### 8. `^varname` not supported in lexer until v1.2.x
444
+
445
+ **Problem:** `^px` (local coordinate with variable offset) was lexed as two
446
+ tokens: `^` (local_coord) + `px` (ident). The parser then failed with
447
+ "Expected ')' but got 'ident'".
448
+
449
+ Only `~varname` (relative coordinate) supported variable names.
450
+ This made the macro-based dynamic particle positioning impossible to write.
451
+
452
+ **Fix applied:** Lexer now reads `^identifier` as a single `local_coord` token.
453
+
454
+ **Fix in redesign:** `^varname` and `~varname` should be unified as
455
+ `LocalCoord(varname | number)` and `RelCoord(varname | number)` in the AST.
456
+
457
+ ---
458
+
459
+ ### 9. sin_fixed is a lookup table, and that is correct
460
+
461
+ Not a pitfall — a deliberate constraint.
462
+
463
+ MC scoreboards support only 32-bit integer arithmetic (add, sub, mul, div, mod).
464
+ There is no trigonometric instruction. Taylor series (`sin x = x - x³/6 + ...`)
465
+ overflows INT32 by the third term in fixed-point ×1000 representation.
466
+ CORDIC requires ~20 integer iterations per call.
467
+
468
+ A 91-entry table (0°–90° with quadrant symmetry) gives exact 1° resolution
469
+ in O(1) and is the standard approach on integer-only platforms (GBA BIOS,
470
+ early DSP chips, NDS).
471
+
472
+ **Implication for redesign:** The `sin_fixed` table pattern (initialized at
473
+ `@load`, read via storage array indexing + macros) is a first-class language
474
+ pattern, not a hack. The stdlib should keep it.
475
+
476
+ ---
477
+
478
+ ### 10. Datapack objective collision (the `rs` problem)
479
+
480
+ **Problem:** All compiled datapacks shared a single scoreboard objective named
481
+ `rs`. Two datapacks in the same world had their mangle tables collide — the
482
+ `$a rs` slot meant different things in each datapack's load function.
483
+
484
+ **Fix applied:** Default objective is now `__<namespace>` (double-underscore
485
+ prefix, following the `__load`/`__tick` convention).
486
+
487
+ **Fix in redesign:** `CompileContext` carries the objective name. No global.
488
+
489
+ ---
490
+
491
+ ## Language Design: TypeScript Syntax, Custom Frontend
492
+
493
+ ### Should we reuse the TypeScript frontend (tsc / ts-morph)?
494
+
495
+ **No.** The core RedScript syntax is not valid TypeScript:
496
+
497
+ ```redscript
498
+ foreach (p in @a[tag=foo, limit=1]) at @s {
499
+ particle("end_rod", ^0, ^0, ^5, 0.02, 0.02, 0.02, 0, 10);
500
+ }
501
+ kill(@e[tag=screen]);
502
+ ```
503
+
504
+ `@a[tag=foo]` is not a valid TS expression (confused with array-access on a
505
+ decorator). `^5` / `~-3` are not valid TS expressions. `at @s {}` does not
506
+ exist. Encoding these as valid TS trades the language for a verbose API:
507
+
508
+ ```typescript
509
+ // not the goal
510
+ forEach(selector('@a', { tag: 'foo', limit: 1 }), (p) =>
511
+ atSelf(p, () => particle('end_rod', localCoord(0, 0, 5), ...)));
512
+ ```
513
+
514
+ If we have to write that, RedScript provides no value. Keep the custom parser.
515
+
516
+ ### Should we reuse TypeScript's type checker (tsc)?
517
+
518
+ **No.** RedScript only needs a small subset of TypeScript's type system:
519
+
520
+ | Feature | TypeScript | RedScript needs |
521
+ |---|---|---|
522
+ | Primitive types | `number`, `string`, `boolean`, `symbol`, `bigint`... | `int`, `bool`, `string`, `float` |
523
+ | Compound | union, intersection, conditional, mapped, template literal | `struct`, `enum`, `T[]` |
524
+ | MC-specific | — | `selector<T>`, `BlockPos`, `void` |
525
+ | Generics | higher-kinded, infer, conditional | simple `T<U>` instantiation |
526
+ | Complexity | Turing-complete type system | intentionally simple |
527
+
528
+ Embedding tsc's type checker means inheriting `never`, `unknown`, conditional
529
+ types, `infer`, mapped types — none of which are useful on the MC target. A
530
+ lightweight structural type checker custom-built for the above set is smaller,
531
+ faster, and easier to extend with MC-specific rules.
532
+
533
+ ### What to borrow from TypeScript (syntax conventions only)
534
+
535
+ Keep the source syntax **familiar to TypeScript developers** without binding to tsc:
536
+
537
+ ```redscript
538
+ // These match TypeScript conventions — keep them
539
+ let x: int = 0;
540
+ const MAX: int = 100;
541
+ fn add(a: int, b: int): int { return a + b; }
542
+ struct Vec2 { x: int; y: int; }
543
+ impl Vec2 {
544
+ fn length(self): int { ... }
545
+ }
546
+ type Callback = (x: int) => void; // function type syntax
547
+ ```
548
+
549
+ ```redscript
550
+ // MC-specific extensions — keep them as-is, do not force into TS grammar
551
+ @tick fn _update() { ... } // decorator-style annotation
552
+ foreach (p in @a[tag=foo]) at @s { } // MC selector iteration
553
+ let s: selector<entity> = @e[...]; // generic selector type
554
+ kill(@e[tag=screen]); // MC command as builtin call
555
+ particle("end_rod", ^px, ^py, ^5, ...) // caret/tilde coordinates
556
+ ```
557
+
558
+ The rule: **syntax form follows TypeScript; semantics follow Minecraft.**
559
+
560
+ ### IDE support: implement LSP, not a tsc plugin
561
+
562
+ For real IntelliSense (completions, hover types, go-to-definition), the correct
563
+ path is a Language Server Protocol implementation:
564
+
565
+ ```
566
+ redscript-lsp
567
+ ├── parse .mcrs → typed AST
568
+ ├── type inference + error diagnostics
569
+ ├── completions: builtin names, selector attributes, struct fields
570
+ ├── hover: type info, MC command documentation
571
+ └── go-to-definition: cross-file symbol resolution
572
+ ```
573
+
574
+ LSP decouples the language server from the editor: VS Code, Neovim, Helix,
575
+ Zed, and any LSP-capable editor get support from one implementation.
576
+ A tsc plugin would be harder, VS Code-only, and still require all the same
577
+ semantic analysis.
578
+
579
+
580
+ ---
581
+
582
+ ## MC Compilation Target: Computational Commands
583
+
584
+ This section covers the MC commands that actually participate in computation —
585
+ not side-effect commands like `particle`, `summon`, `say`, `playsound`, etc.
586
+ Every operation in the IR must ultimately map to one or more of these.
587
+
588
+ ### Scoreboard: the "CPU registers" of MC
589
+
590
+ Scoreboard objectives hold named fake-player slots, each storing one INT32.
591
+ This is the primary computational medium.
592
+
593
+ ```
594
+ # Initialization
595
+ scoreboard objectives add <obj> dummy
596
+
597
+ # Write constant
598
+ scoreboard players set <fake_player> <obj> <value>
599
+
600
+ # Copy
601
+ scoreboard players operation $dst <obj> = $src <obj>
602
+
603
+ # Arithmetic (all in-place, 2-address)
604
+ scoreboard players operation $dst <obj> += $src <obj> # add
605
+ scoreboard players operation $dst <obj> -= $src <obj> # sub
606
+ scoreboard players operation $dst <obj> *= $src <obj> # mul
607
+ scoreboard players operation $dst <obj> /= $src <obj> # integer div (truncates toward zero)
608
+ scoreboard players operation $dst <obj> %= $src <obj> # mod (sign follows dividend)
609
+
610
+ # Min / max
611
+ scoreboard players operation $dst <obj> < $src <obj> # dst = min(dst, src)
612
+ scoreboard players operation $dst <obj> > $src <obj> # dst = max(dst, src)
613
+
614
+ # Swap
615
+ scoreboard players operation $a <obj> >< $b <obj>
616
+ ```
617
+
618
+ **Constraints:**
619
+ - INT32 only. No float, no 64-bit.
620
+ - Division is truncated toward zero (Java `int` semantics).
621
+ - No bitwise operations. XOR/AND/OR must be emulated with arithmetic.
622
+ - No comparison that produces a value — comparisons only appear in `execute if score`.
623
+
624
+ ### `execute store result score` — bridge from commands to scores
625
+
626
+ Captures the integer result of a command into a score slot:
627
+
628
+ ```
629
+ execute store result score $dst <obj> run <command>
630
+ ```
631
+
632
+ Used for:
633
+ - Reading entity NBT: `run data get entity @s Health 1`
634
+ - Reading storage: `run data get storage <ns> <path> <scale>`
635
+ - Capturing command success: `execute store success score $dst <obj> run ...`
636
+ - Returning from a function: `run function <ns>:<fn>` (captures `return` value)
637
+
638
+ ### `execute if/unless score` — the only conditional
639
+
640
+ All control flow in the compiled output is expressed with score comparisons:
641
+
642
+ ```
643
+ # Range check (most common — if score matches an integer range)
644
+ execute if score $x <obj> matches <N> run function <ns>:then_block
645
+ execute if score $x <obj> matches <N>..<M> run function <ns>:range_block
646
+ execute if score $x <obj> matches ..<N> run function <ns>:le_block
647
+
648
+ # Two-operand comparison
649
+ execute if score $a <obj> = $b <obj> run ... # a == b
650
+ execute if score $a <obj> < $b <obj> run ... # a < b
651
+ execute unless score $a <obj> = $b <obj> run ... # a != b
652
+ ```
653
+
654
+ **Why `matches` vs two-operand:**
655
+ - `matches N..` is cheaper than `= $const` (no extra fake-player needed).
656
+ - `matches 1..` is the canonical boolean-true check.
657
+ - Two-operand form needed for dynamic comparisons (`a < b` where both vary).
658
+
659
+ ### NBT Storage: heap memory
660
+
661
+ NBT storage (`data storage <ns>:<path>`) is the only persistent structured
662
+ memory available. It holds typed NBT values (int, double, string, list, compound).
663
+
664
+ ```
665
+ # Write literal
666
+ data modify storage <ns> <path> set value <nbt_literal>
667
+
668
+ # Copy between paths
669
+ data modify storage <ns> <dst_path> set from storage <ns> <src_path>
670
+
671
+ # Read into score (with optional scale factor)
672
+ execute store result score $dst <obj> run data get storage <ns> <path> 1
673
+
674
+ # Write score into storage (with scale: useful for float conversion)
675
+ execute store result storage <ns> <path> int 1 run scoreboard players get $src <obj>
676
+ execute store result storage <ns> <path> double 0.01 run scoreboard players get $src <obj>
677
+ # → stores (score × 0.01) as a double; e.g. score=975 → NBT 9.75d
678
+ ```
679
+
680
+ **Scale factor:**
681
+ The `<scale>` in `data get` / `execute store result storage` is a multiplier
682
+ applied on read/write. This is the only way to convert between integer scores
683
+ and fractional NBT values (used for float-coordinate macro parameters).
684
+
685
+ ### Array indexing via NBT
686
+
687
+ Static index (compile-time constant):
688
+
689
+ ```
690
+ execute store result score $dst <obj> run data get storage rs:heap array[5] 1
691
+ data modify storage rs:heap array[3] set value 42
692
+ ```
693
+
694
+ Dynamic index (runtime variable) — requires MC 1.20.2+ macros:
695
+
696
+ ```
697
+ # Step 1: write index into macro args storage
698
+ execute store result storage rs:macro_args i int 1 run scoreboard players get $idx <obj>
699
+
700
+ # Step 2: call macro function
701
+ function ns:_read_array with storage rs:macro_args
702
+
703
+ # Step 3: inside _read_array.mcfunction (macro function)
704
+ $execute store result score $ret <obj> run data get storage rs:heap array[$(i)] 1
705
+ ```
706
+
707
+ **RedScript `storage_get_int` / `storage_set_int` builtins compile to exactly this pattern.**
708
+
709
+ ### MC 1.20.2+ Function Macros
710
+
711
+ A function file that contains `$(key)` substitutions must be called with
712
+ `function <ns>:<fn> with storage <ns>:<macro_storage>`.
713
+ Any line containing `$(...)` must begin with `$`.
714
+
715
+ ```
716
+ # Caller: populate rs:macro_args, then call
717
+ execute store result storage rs:macro_args px double 0.01 run scoreboard players get $px_int <obj>
718
+ execute store result storage rs:macro_args py double 0.01 run scoreboard players get $py_int <obj>
719
+ function rsdemo:_draw with storage rs:macro_args
720
+
721
+ # Inside _draw.mcfunction (macro function):
722
+ $particle minecraft:end_rod ^$(px) ^$(py) ^5 0.02 0.02 0.02 0 10
723
+ ```
724
+
725
+ **What macros unlock:**
726
+ - Dynamic array indexing (above)
727
+ - Dynamic coordinates in `particle`, `setblock`, `tp`, `fill`, etc.
728
+ - Dynamic entity selectors and NBT paths
729
+
730
+ **Constraint:** Macro substitution is string interpolation at the command level.
731
+ The substituted value must be a valid literal for that position (integer, float,
732
+ coordinate, selector string). No arithmetic is performed during substitution.
733
+
734
+ ### `function` and `return`: call graph and early exit
735
+
736
+ ```
737
+ # Unconditional call
738
+ function <ns>:<path>
739
+
740
+ # Conditional call (the compiled form of if/else branches)
741
+ execute if score $cond <obj> matches 1.. run function <ns>:then_0
742
+ execute if score $cond <obj> matches ..0 run function <ns>:else_0
743
+
744
+ # Macro function call
745
+ function <ns>:<path> with storage <ns>:<macro_storage>
746
+
747
+ # Return a value (MC 1.20.3+)
748
+ return 42
749
+ return run scoreboard players get $x <obj>
750
+
751
+ # Early return (MC 1.20.2+, exits current function immediately)
752
+ return 0
753
+ ```
754
+
755
+ `return run <cmd>` stores the command's result as the function's return value,
756
+ readable via `execute store result score $ret <obj> run function ...`.
757
+
758
+ ### Summary: IR operation → MC command mapping
759
+
760
+ | IR operation | MC command |
761
+ |---|---|
762
+ | `x = const N` | `scoreboard players set $x <obj> N` |
763
+ | `x = copy y` | `scoreboard players operation $x <obj> = $y <obj>` |
764
+ | `x = add y, z` | copy y→x, then `+= $z` |
765
+ | `x = sub y, z` | copy y→x, then `-= $z` |
766
+ | `x = mul y, z` | copy y→x, then `*= $z` |
767
+ | `x = div y, z` | copy y→x, then `/= $z` |
768
+ | `x = mod y, z` | copy y→x, then `%= $z` |
769
+ | `if x == 0` | `execute if score $x <obj> matches 0 run ...` |
770
+ | `if x > y` | `execute if score $x <obj> > $y <obj> run ...` |
771
+ | `x = array[i]` | macro: store i, call macro fn, read `$ret` |
772
+ | `array[i] = v` | macro: store i+v, call macro fn |
773
+ | `call fn(args...)` | set up params in `$p0..$pN`, `function <ns>:<fn>` |
774
+ | `call_macro fn(args...)` | store args in `rs:macro_args`, `function ... with storage` |
775
+ | `return x` | `scoreboard players operation $ret <obj> = $x <obj>` |
776
+
777
+
778
+ ---
779
+
780
+ ## Current Debugging & Tooling
781
+
782
+ ### CLI commands
783
+
784
+ ```
785
+ redscript compile <file> [-o <out>] [--namespace <ns>] [--scoreboard <obj>] [--no-dce] [--no-mangle]
786
+ redscript watch <dir> [-o <out>] [--namespace <ns>] [--hot-reload <url>]
787
+ redscript check <file>
788
+ redscript fmt <file> [file2 ...]
789
+ redscript repl
790
+ redscript generate-dts [-o <file>]
791
+ ```
792
+
793
+ ---
794
+
795
+ ### `redscript check` — local syntax checker
796
+
797
+ **What it does:** Parse + preprocess only. Exits 0 if the file is syntactically
798
+ valid, non-zero with a formatted error otherwise. Does **not** run the type
799
+ checker, lowering, or optimizer.
800
+
801
+ ```bash
802
+ redscript check examples/readme-demo.mcrs
803
+ # ✓ examples/readme-demo.mcrs is valid
804
+ ```
805
+
806
+ **Known bug:** `check` always passes `namespace = 'redscript'` hardcoded to the
807
+ parser, regardless of the filename or any `--namespace` flag. This means
808
+ namespace-sensitive parse errors (e.g. a symbol that happens to conflict with
809
+ the literal string `"redscript"`) may behave differently under `check` vs
810
+ `compile`. Fix: derive namespace from filename (same logic as `compile`) or
811
+ accept a `--namespace` flag.
812
+
813
+ Additionally, `check` only calls the parser — it does **not** call the type
814
+ checker (`TypeChecker`). Type errors silently pass `check` and only surface
815
+ during `compile`. The type checker itself is currently in "warn mode" (collects
816
+ errors but does not block compilation), so even `compile` does not hard-fail on
817
+ type errors.
818
+
819
+ **Redesign:** `check` should run: parse → name resolution → full type checking,
820
+ and exit non-zero on any diagnostic. The current "warn mode" type checker should
821
+ become "error mode".
822
+
823
+ ---
824
+
825
+ ### `redscript watch` + `--hot-reload` — live reload against a running server
826
+
827
+ Watch mode recompiles on every `.mcrs` file change in a directory, then
828
+ optionally POSTs to a hot-reload endpoint:
829
+
830
+ ```bash
831
+ redscript watch src/ -o ~/mc-test-server/world/datapacks/rsdemo \
832
+ --namespace rsdemo \
833
+ --hot-reload http://localhost:25570
834
+ ```
835
+
836
+ On each successful compile, it calls `POST <url>/reload`, which is expected
837
+ to trigger `/reload` on the MC server. This gives a **save → auto-deploy →
838
+ `/reload`** loop without switching to the game.
839
+
840
+ ```
841
+ Save .mcrs
842
+ → recompile (< 1 s)
843
+ → write .mcfunction files to datapack dir
844
+ → POST /reload
845
+ → server reloads datapack
846
+ → test in-game immediately
847
+ ```
848
+
849
+ The hot-reload server is a tiny HTTP listener that must be running alongside
850
+ the MC server. Currently this is a manual setup (run a small HTTP server that
851
+ calls RCON `/reload`).
852
+
853
+ **Known limitation:** watch mode compiles all `.mcrs` files in the directory on
854
+ every change, not just the changed file. For large projects this is wasteful.
855
+ Incremental compilation (track which files changed, only recompile affected
856
+ functions) is a future improvement.
857
+
858
+ ---
859
+
860
+ ### `redscript repl` — interactive expression evaluator
861
+
862
+ Starts a read-eval-print loop. Accepts RedScript expressions and statements,
863
+ compiles them, runs them through `MCRuntime` (the in-process scoreboard
864
+ simulator), and prints the result.
865
+
866
+ Useful for quickly testing arithmetic, `sin_fixed` values, or algorithm
867
+ correctness without deploying to a server.
868
+
869
+ ```
870
+ > let x: int = sin_fixed(45);
871
+ x = 707
872
+ > x * x + (cos_fixed(45) * cos_fixed(45) / 1000)
873
+ = 999649
874
+ ```
875
+
876
+ **Known limitation:** the REPL resets all state between expressions (no
877
+ persistent variable binding across lines). Calling stdlib functions that depend
878
+ on `@load` initialization (e.g. `sin_fixed` table load) may not work correctly
879
+ unless the REPL explicitly runs the load function first.
880
+
881
+ ---
882
+
883
+ ### `--no-mangle` flag — readable variable names for debugging
884
+
885
+ By default, IR variable names are mangled to short names (`$a`, `$b`, `$ad`...)
886
+ to keep scoreboard objective slot names short. With `--no-mangle`, the original
887
+ source variable names are preserved:
888
+
889
+ ```bash
890
+ redscript compile demo.mcrs -o /tmp/out --no-mangle
891
+ ```
892
+
893
+ Generated mcfunction uses `$phase __rsdemo` instead of `$c __rsdemo`, making
894
+ it possible to read the output and correlate with source.
895
+
896
+ ---
897
+
898
+ ### Sourcemap
899
+
900
+ Every compile outputs a `.map.json` file alongside the datapack:
901
+
902
+ ```
903
+ /tmp/rsdemo/rsdemo.map.json
904
+ ```
905
+
906
+ Maps each generated `.mcfunction` path back to the source `.mcrs` file and
907
+ line number. Currently used for error reporting. Future use: step-through
908
+ debugger that maps MC function calls back to source lines.
909
+
910
+ ---
911
+
912
+ ### `MCRuntime` — in-process MC simulator (used by tests)
913
+
914
+ `src/runtime/index.ts` implements a simulated MC execution environment:
915
+ - Scoreboard: fake-player → objective → INT32 value
916
+ - NBT storage: nested map of NBT values
917
+ - Function call stack: dispatches `function ns:path` to the compiled output
918
+ - `execute if/unless score`: evaluated against the simulated scoreboard
919
+ - `execute store result score ... run ...`: captures command return value
920
+
921
+ All 920 tests use `MCRuntime` to run compiled datapacks in-process without a
922
+ real MC server. This makes the test suite fast (< 35 s for all 920 tests) and
923
+ server-independent.
924
+
925
+ **What `MCRuntime` does not simulate:**
926
+ - Entity selectors (`@a`, `@e`, `@p`, `@s`) — tests must mock these
927
+ - World block state (`setblock`, `fill`) — not tracked
928
+ - Particle/sound/title commands — silently ignored
929
+ - Tick scheduling — tests call `@tick` functions manually
930
+
931
+ ---
932
+
933
+ ### Test server: Paper 1.21.4 at `~/mc-test-server`
934
+
935
+ For integration testing that requires real MC behavior (entity selectors,
936
+ actual particle rendering, boss bars, etc.):
937
+
938
+ ```bash
939
+ # Start
940
+ cd ~/mc-test-server
941
+ /opt/homebrew/opt/openjdk@21/bin/java -jar paper.jar --nogui
942
+
943
+ # Deploy a datapack
944
+ redscript compile examples/readme-demo.mcrs \
945
+ -o ~/mc-test-server/world/datapacks/rsdemo \
946
+ --namespace rsdemo
947
+
948
+ # In-game or via RCON
949
+ /reload
950
+ /function rsdemo:start
951
+ ```
952
+
953
+ Server details:
954
+ - Paper 1.21.4-232
955
+ - Port 25561
956
+ - Java: `/opt/homebrew/opt/openjdk@21/bin/java`
957
+ - Accessible via Tailscale at `100.73.231.27:25561`
958
+
959
+
960
+ ---
961
+
962
+ ## Language Semantics Design (Redesign Decisions)
963
+
964
+ ### Visibility & DCE: `export` replaces `@keep`
965
+
966
+ **Current:** `@keep` forces a function to survive DCE. Everything without `@keep`
967
+ is potentially eliminated if unreachable.
968
+
969
+ **Redesign:** Use `export` as the explicit public-API marker, matching TypeScript/JS conventions.
970
+
971
+ ```redscript
972
+ export fn spawn_wave() { ... } // public — never DCE'd
973
+ fn _helper(x: int): int { ... } // private — eliminated if unreachable
974
+
975
+ @tick fn _tick() { ... } // @tick implies export (referenced by tick.json)
976
+ @load fn _load() { ... } // @load implies export (referenced by load.json)
977
+ ```
978
+
979
+ Rules:
980
+ - `export` → never DCE'd; accessible from other datapacks / MC
981
+ - no `export` → private; DCE applies
982
+ - `@tick` / `@load` implicitly export the function and wire it into `tick.json` / `load.json`
983
+ - `@require_on_load` (current stdlib pragma) → absorbed into `@load` or library `export` semantics
984
+
985
+ `module library;` pragma stays: marks a file as a library (all exports are
986
+ available for import; nothing auto-runs at load time).
987
+
988
+ ---
989
+
990
+ ### Struct: value type, no heap, no references
991
+
992
+ `struct` is kept (not renamed to `class`) because the value-type semantics are
993
+ immediately obvious from the name — same as C/C++/Rust structs.
994
+
995
+ ```redscript
996
+ struct Vec2 {
997
+ x: int;
998
+ y: int;
999
+ }
1000
+
1001
+ impl Vec2 {
1002
+ fn length_sq(self): int {
1003
+ return self.x * self.x + self.y * self.y;
1004
+ }
1005
+ }
1006
+
1007
+ let v: Vec2 = Vec2 { x: 3, y: 4 };
1008
+ let d: int = v.length_sq(); // = 25
1009
+ ```
1010
+
1011
+ **Constraints (by MC target):**
1012
+ - No heap allocation. A `Vec2` is two scoreboard slots, not a pointer.
1013
+ - No references. `let a = v; a.x = 10` does **not** modify `v`.
1014
+ - No dynamic dispatch / vtables. Method calls are statically resolved at compile time.
1015
+ - No inheritance. Composition only.
1016
+ - Struct fields cannot be `string` (strings cannot live in scoreboard).
1017
+
1018
+ **Why not `class`?** "Class" implies heap allocation and reference semantics in
1019
+ most languages. Using `struct` sets the correct expectation: this is a named
1020
+ group of scoreboard slots, not a Java-style object.
1021
+
1022
+ ---
1023
+
1024
+ ### Macro functions: transparent to users
1025
+
1026
+ A **macro function** is one that uses a parameter as a dynamic MC coordinate or
1027
+ array index — positions where MC requires literal values but we want runtime
1028
+ substitution via the 1.20.2+ function macro mechanism.
1029
+
1030
+ ```redscript
1031
+ // User writes this — looks like a normal function:
1032
+ fn draw_pt(px: float, py: float) {
1033
+ particle("minecraft:end_rod", ^px, ^py, ^5, 0.02, 0.02, 0.02, 0.0, 10);
1034
+ }
1035
+ ```
1036
+
1037
+ The compiler detects that `px`/`py` appear in `^`-coordinate positions and
1038
+ automatically emits:
1039
+ 1. A macro function file (`$particle ... ^$(px) ^$(py) ...`)
1040
+ 2. A call site that writes args to `rs:macro_args` storage and calls
1041
+ `function ns:draw_pt with storage rs:macro_args`
1042
+
1043
+ **Users never write `$` or `with storage`** — the compiler handles it.
1044
+
1045
+ In the redesign, macro-function status can be auto-detected (current behavior)
1046
+ or explicitly annotated `@macro fn draw_pt(...)`. Auto-detection is simpler for
1047
+ users; explicit annotation makes it clearer in large codebases. Decision: keep
1048
+ auto-detection, but emit a diagnostic if a function is unexpectedly promoted to
1049
+ macro status (so users are aware).
1050
+
1051
+ ---
1052
+
1053
+ ### Error handling & diagnostics
1054
+
1055
+ **Goal:** report all errors in a file before stopping, not just the first one.
1056
+ This is critical for IDE integration (the language server must not crash on the
1057
+ first typo).
1058
+
1059
+ **Approach: panic-mode error recovery in the parser**
1060
+
1061
+ When the parser encounters an unexpected token, it:
1062
+ 1. Records the error with source span
1063
+ 2. Skips tokens until it finds a synchronization point: `fn`, `}`, `;`, `@tick`, `@load`, EOF
1064
+ 3. Resumes parsing from that point
1065
+
1066
+ This collects multiple independent errors before stopping:
1067
+
1068
+ ```
1069
+ Error at line 5: expected ':' but got '='
1070
+ Error at line 12: unknown type 'flot'
1071
+ Error at line 18: undefined variable 'phse'
1072
+ 3 errors found.
1073
+ ```
1074
+
1075
+ **Not doing incremental parsing.** Incremental parsing (re-parse only changed
1076
+ sections) is a separate project requiring a tree-sitter-style persistent parse
1077
+ tree. The benefit for RedScript's typical file sizes (< 500 lines) is minimal,
1078
+ and the implementation cost is high. Panic-mode recovery is sufficient for
1079
+ a good developer experience.
1080
+
1081
+ **Diagnostic severity levels:**
1082
+
1083
+ | Level | Use |
1084
+ |---|---|
1085
+ | `error` | Compilation fails. Type mismatch, undefined symbol, syntax error. |
1086
+ | `warning` | Compilation succeeds but something is suspicious. Unused variable, unreachable code. |
1087
+ | `hint` | Informational. Style suggestions, implicit conversions. |
1088
+
1089
+ **Current `TypeChecker` is in "warn mode"** (type errors do not block compilation).
1090
+ In the redesign, type errors are `error` level and do block compilation.
1091
+
1092
+ ---
1093
+
1094
+ ### Type system
1095
+
1096
+ #### Primitive types
1097
+
1098
+ | Type | Storage | Notes |
1099
+ |---|---|---|
1100
+ | `int` | scoreboard INT32 | All arithmetic. Range: −2³¹ to 2³¹−1 |
1101
+ | `float` | scoreboard INT32 (×1000) | Fixed-point. `1.5` stored as `1500`. Use `mulfix`/`divfix` for ×/÷ |
1102
+ | `bool` | scoreboard 0 or 1 | `true`=1, `false`=0 |
1103
+ | `string` | NBT string | Cannot do arithmetic. Only usable in command/NBT contexts |
1104
+ | `void` | — | Function returns nothing |
1105
+
1106
+ #### MC-specific types
1107
+
1108
+ | Type | Notes |
1109
+ |---|---|
1110
+ | `selector<entity>` | Not a runtime value. Only usable in `foreach` / command contexts |
1111
+ | `selector<player>` | Subtype of `selector<entity>` for player-only selectors |
1112
+ | `BlockPos` | Coordinate triple. Only usable in command contexts |
1113
+
1114
+ #### Compound types
1115
+
1116
+ | Type | Notes |
1117
+ |---|---|
1118
+ | `struct Foo { ... }` | Value type. Fields are independent scoreboard slots |
1119
+ | `int[]` | NBT integer array. Dynamic access requires macro |
1120
+ | `(a: int) => int` | Function type. Used for stdlib callbacks |
1121
+
1122
+ #### Key design decisions
1123
+
1124
+ **1. Nominal typing (not structural)**
1125
+
1126
+ Two structs with identical fields are NOT compatible:
1127
+
1128
+ ```redscript
1129
+ struct Vec2 { x: int; y: int; }
1130
+ struct Point { x: int; y: int; }
1131
+
1132
+ fn distance(a: Vec2, b: Vec2): int { ... }
1133
+ let p: Point = Point { x: 1, y: 2 };
1134
+ distance(p, p) // ERROR: Point is not Vec2
1135
+ ```
1136
+
1137
+ Rationale: `Vec2` and `Point` use different scoreboard slot names. Structural
1138
+ compatibility would require a copy — which must be explicit.
1139
+
1140
+ **2. No implicit type conversion**
1141
+
1142
+ ```redscript
1143
+ let x: int = 5;
1144
+ let y: float = x; // ERROR: use 'x as float' (×1000 implicit conversion)
1145
+ let z: float = x as float; // OK: z = 5000 internally
1146
+ ```
1147
+
1148
+ Rationale: `int → float` is a ×1000 multiply. Making it implicit hides a
1149
+ potentially significant operation and makes arithmetic bugs hard to find.
1150
+
1151
+ **3. No null**
1152
+
1153
+ Scoreboard slots are always initialized to 0. There is no null/undefined/None
1154
+ in RedScript. No nullable types, no optional chaining.
1155
+
1156
+ **4. No union types, no conditional types**
1157
+
1158
+ These require runtime type tags, which cost scoreboard slots and add dispatch
1159
+ overhead. Not worth it at MC scale.
1160
+
1161
+ **5. Simple generics only**
1162
+
1163
+ `selector<entity>` and `selector<player>` are essentially two distinct concrete
1164
+ types, not a generic in the full sense. Generic functions (e.g. stdlib
1165
+ `foreach<T>`) are specialized at each call site by the compiler — no runtime
1166
+ polymorphism.
1167
+
1168
+ #### Type inference
1169
+
1170
+ Local variables: `let x = 5` → `int`, `let x = 5.0` → `float`, `let x = true` → `bool`.
1171
+ Function return types: inferred from `return` statements if not annotated.
1172
+ Struct fields: must be explicitly typed.
1173
+
1174
+ **Implementation estimate:** ~800–1200 lines of TypeScript. Two weeks.
1175
+
1176
+ ---
1177
+
1178
+ ### Incremental compilation: explicitly deferred
1179
+
1180
+ **Decision: do not implement.** `watch` mode already recompiles in < 1 second
1181
+ for typical project sizes. The complexity of tracking a file-level dependency
1182
+ graph and invalidating only affected functions outweighs the benefit.
1183
+
1184
+ If project sizes grow to hundreds of files and compilation becomes noticeably
1185
+ slow, incremental compilation can be added as a separate pass: build a module
1186
+ dependency DAG, recompile only the subgraph invalidated by a file change.
1187
+ Architecture note for the future: keep module loading separated from lowering
1188
+ so that a cached module's IR can be reused without re-parsing.
1189
+
1190
+
1191
+ ---
1192
+
1193
+ ## MC Execution Budget & Coroutine Transform
1194
+
1195
+ ### The 65536 command budget
1196
+
1197
+ `maxCommandChainLength` (gamerule, default 65536) limits the **total number of
1198
+ commands executed per game tick**, summed across all function calls triggered
1199
+ in that tick. It is a tick budget, not a per-function call depth limit.
1200
+
1201
+ A separate limit (~512 levels) applies to nested function call depth (JVM stack).
1202
+ This rarely matters for compiled output, which is usually shallow chains of
1203
+ `execute if ... run function`.
1204
+
1205
+ **Practical implications:**
1206
+ - A loop of 1000 iterations × 10 commands/iteration = 10,000 commands. Safe.
1207
+ - A loop of 1000 iterations × 100 commands/iteration = 100,000 commands. Exceeds budget — only ~655 iterations actually run; the rest are silently dropped.
1208
+ - The budget can be raised by a server admin: `gamerule maxCommandChainLength 1000000`. Cannot be changed from within a datapack.
1209
+
1210
+ **Compiler response:** The redesigned compiler should statically estimate the
1211
+ command count of loops and emit a `warning` when the estimated count approaches
1212
+ the budget.
1213
+
1214
+ ---
1215
+
1216
+ ### Coroutine Transform: automatic tick-splitting
1217
+
1218
+ For computations that genuinely require more commands than one tick allows,
1219
+ the compiler can automatically transform a long-running function into a
1220
+ tick-spread state machine. This is the same transformation JavaScript engines
1221
+ apply to `async/await`, Python applies to `yield`, and C# applies to
1222
+ `yield return` — just targeting the MC tick scheduler instead of an event loop.
1223
+
1224
+ #### Usage (proposed syntax)
1225
+
1226
+ ```redscript
1227
+ @coroutine(batch=10, onDone=after_process_all)
1228
+ fn process_all() {
1229
+ for (let i: int = 0; i < 1000; i++) {
1230
+ do_work(i); // heavy per-iteration work
1231
+ }
1232
+ }
1233
+
1234
+ fn after_process_all() {
1235
+ finish();
1236
+ }
1237
+ ```
1238
+
1239
+ `@coroutine(batch=N)` tells the compiler: "split this function's loops so that
1240
+ each tick advances at most N iterations". If `batch` is omitted, the compiler
1241
+ estimates it from the loop body's command count.
1242
+
1243
+ `onDone=<fn>` is an optional completion callback. When the coroutine reaches
1244
+ `pc = -1` (done), the runtime schedules `onDone` once on the next tick.
1245
+ This covers most "await-like" workflows without introducing `await` syntax
1246
+ or Promise/Future semantics into the language.
1247
+
1248
+ #### How the transform works
1249
+
1250
+ **Step 1: Find yield points**
1251
+
1252
+ In the MIR CFG, find all back edges (edges that jump to a dominator block —
1253
+ i.e., loop headers). A yield point is inserted at each back edge, triggered
1254
+ every `batch` iterations.
1255
+
1256
+ **Step 2: Liveness analysis at yield points**
1257
+
1258
+ Compute the set of variables live at each yield point. These variables must
1259
+ persist across ticks — they cannot stay in function-local temporary slots.
1260
+ They are promoted to persistent scoreboard slots (or NBT for arrays/structs).
1261
+
1262
+ In the example above, `i` is the only live variable at the loop's yield point.
1263
+
1264
+ **Step 3: Split the CFG into continuations**
1265
+
1266
+ Each segment between yield points becomes a separate function. A `pc`
1267
+ (program counter) scoreboard slot tracks which continuation runs next.
1268
+
1269
+ ```
1270
+ coroutine_state:
1271
+ i → $coro_i __ns (promoted temp)
1272
+ pc → $coro_pc __ns (program counter)
1273
+
1274
+ continuation_1 (loop body, batch iterations):
1275
+ for (batch_count = 0; batch_count < BATCH && i < 1000; batch_count++) {
1276
+ do_work(i)
1277
+ i++
1278
+ }
1279
+ if i >= 1000: pc = 2 # advance to finish()
1280
+ else: pc = 1 # resume loop next tick
1281
+ return # end this tick's work
1282
+
1283
+ continuation_2:
1284
+ finish()
1285
+ pc = -1 # done
1286
+ ```
1287
+
1288
+ **Step 4: Generate the dispatcher**
1289
+
1290
+ ```redscript
1291
+ @tick fn _coro_process_all_tick() {
1292
+ // generated — do not edit
1293
+ execute if score $coro_pc __ns matches 1 run function ns:_coro_cont_1
1294
+ execute if score $coro_pc __ns matches 2 run function ns:_coro_cont_2
1295
+ }
1296
+ ```
1297
+
1298
+ The original `process_all()` call site becomes:
1299
+ ```
1300
+ scoreboard players set $coro_pc __ns 1 # start from continuation_1
1301
+ scoreboard players set $coro_i __ns 0 # initialize i
1302
+ ```
1303
+
1304
+ #### Algorithm components
1305
+
1306
+ | Component | Technique | Est. size |
1307
+ |---|---|---|
1308
+ | Find yield points | Dominator tree + back-edge detection | ~100 lines |
1309
+ | Live variable analysis | Standard dataflow (backwards liveness) | ~150 lines |
1310
+ | CFG splitting | Insert `pc = N; return` at yield points | ~200 lines |
1311
+ | Variable promotion | Assign persistent slots to live vars at yields | ~100 lines |
1312
+ | Dispatch generation | `execute if score pc matches N` chain | ~50 lines |
1313
+ | Batch size estimation | Static command-count estimate per loop body | ~100 lines |
1314
+
1315
+ **Total:** ~700 lines. Approximately 3–4 weeks of focused implementation.
1316
+ Prerequisite: proper MIR CFG with dominator tree (Stage 3 of the new pipeline).
1317
+
1318
+ #### Placement in the pipeline
1319
+
1320
+ This transform runs in **Stage 4 (MIR optimization passes)** as an opt-in pass:
1321
+
1322
+ ```typescript
1323
+ const pipeline: Pass[] = [
1324
+ constantFold,
1325
+ copyProp,
1326
+ dce,
1327
+ ...(options.coroutine ? [coroutineTransform] : []), // opt-in
1328
+ destinationForwarding,
1329
+ blockMerge,
1330
+ ]
1331
+ ```
1332
+
1333
+ It is not run by default — only on functions annotated `@coroutine`. The
1334
+ compiler will warn if a non-annotated loop is estimated to exceed the tick
1335
+ budget, suggesting the user add `@coroutine`.
1336
+
1337
+ #### Why no `await` in v1 coroutine
1338
+
1339
+ This feature is for **tick-slicing** (budget control), not asynchronous IO.
1340
+ Minecraft execution is single-threaded and tick-driven, so a state-machine +
1341
+ `onDone` callback model is enough for most needs while keeping language
1342
+ complexity low. If needed later, `join()` / `is_done()` / `get_result()` can be
1343
+ added on top without changing the core transform.
1344
+
1345
+ #### What the transform does NOT do
1346
+
1347
+ - Does not propagate coroutine return values in v1; completion is signaled via `onDone` callback (no value payload yet).
1348
+ - Does not support nested coroutines (a `@coroutine` calling another `@coroutine`).
1349
+ - Does not handle exceptions (RedScript has none, so this is fine).
1350
+ - Does not parallelize — MC is single-threaded; `@tick` runs one coroutine step per tick.
1351
+
1352
+ #### Relationship to Timer and manual state machines
1353
+
1354
+ RedScript's `Timer` stdlib is a manually written version of this pattern.
1355
+ The coroutine transform automates what users currently write by hand
1356
+ when they need multi-tick computations.
1357
+
1358
+
1359
+ ---
1360
+
1361
+ ## MIR Instruction Set (Specification)
1362
+
1363
+ MIR is 3-address, versioned temporaries, explicit CFG.
1364
+ Every instruction produces at most one result into a fresh temporary.
1365
+
1366
+ ### Types
1367
+
1368
+ ```typescript
1369
+ // A temporary variable — unique within a function, named t0, t1, t2...
1370
+ type Temp = string
1371
+
1372
+ // An operand: either a temp or an inline constant
1373
+ type Operand =
1374
+ | { kind: 'temp', name: Temp }
1375
+ | { kind: 'const', value: number }
1376
+
1377
+ // A basic block identifier
1378
+ type BlockId = string
1379
+
1380
+ // Comparison operators (for cmp instruction)
1381
+ type CmpOp = 'eq' | 'ne' | 'lt' | 'le' | 'gt' | 'ge'
1382
+
1383
+ // NBT value types (for nbt_write)
1384
+ type NBTType = 'int' | 'double' | 'float' | 'long' | 'short' | 'byte'
1385
+ ```
1386
+
1387
+ ### Instructions
1388
+
1389
+ ```typescript
1390
+ type MIRInstr =
1391
+ // ── Constants & copies ──────────────────────────────────────────────────
1392
+ | { kind: 'const', dst: Temp, value: number }
1393
+ // dst = value
1394
+
1395
+ | { kind: 'copy', dst: Temp, src: Operand }
1396
+ // dst = src
1397
+
1398
+ // ── Integer arithmetic ──────────────────────────────────────────────────
1399
+ | { kind: 'add', dst: Temp, a: Operand, b: Operand }
1400
+ | { kind: 'sub', dst: Temp, a: Operand, b: Operand }
1401
+ | { kind: 'mul', dst: Temp, a: Operand, b: Operand }
1402
+ | { kind: 'div', dst: Temp, a: Operand, b: Operand } // truncated toward zero
1403
+ | { kind: 'mod', dst: Temp, a: Operand, b: Operand } // sign follows dividend
1404
+ | { kind: 'neg', dst: Temp, src: Operand } // dst = -src
1405
+
1406
+ // ── Comparison (result is 0 or 1) ────────────────────────────────────────
1407
+ | { kind: 'cmp', dst: Temp, op: CmpOp, a: Operand, b: Operand }
1408
+
1409
+ // ── Boolean logic ────────────────────────────────────────────────────────
1410
+ | { kind: 'and', dst: Temp, a: Operand, b: Operand }
1411
+ | { kind: 'or', dst: Temp, a: Operand, b: Operand }
1412
+ | { kind: 'not', dst: Temp, src: Operand }
1413
+
1414
+ // ── NBT storage ──────────────────────────────────────────────────────────
1415
+ | { kind: 'nbt_read',
1416
+ dst: Temp,
1417
+ ns: string, path: string,
1418
+ scale: number }
1419
+ // Compiles to: execute store result score $dst <obj> run data get storage ns path scale
1420
+
1421
+ | { kind: 'nbt_write',
1422
+ ns: string, path: string,
1423
+ type: NBTType, scale: number,
1424
+ src: Operand }
1425
+ // Compiles to: execute store result storage ns path type scale run scoreboard players get $src <obj>
1426
+ // or: data modify storage ns path set value <literal> (when src is const)
1427
+
1428
+ // ── Function calls ────────────────────────────────────────────────────────
1429
+ | { kind: 'call',
1430
+ dst: Temp | null,
1431
+ fn: string,
1432
+ args: Operand[] }
1433
+ // Regular function call. args are passed via $p0..$pN scoreboard slots.
1434
+
1435
+ | { kind: 'call_macro',
1436
+ dst: Temp | null,
1437
+ fn: string,
1438
+ args: { name: string, value: Operand, type: NBTType, scale: number }[] }
1439
+ // Macro function call. args written to rs:macro_args storage.
1440
+ // Compiles to: [store each arg] + function fn with storage rs:macro_args
1441
+
1442
+ | { kind: 'call_context',
1443
+ fn: string,
1444
+ subcommands: ExecuteSubcmd[] }
1445
+ // execute [subcommands] run function fn
1446
+ // No return value (execute context calls are void).
1447
+
1448
+ // ── Terminators (exactly one per basic block, must be last) ──────────────
1449
+ | { kind: 'jump', target: BlockId }
1450
+ // Unconditional jump.
1451
+
1452
+ | { kind: 'branch', cond: Operand, then: BlockId, else: BlockId }
1453
+ // Conditional jump. cond must be 0 or 1.
1454
+
1455
+ | { kind: 'return', value: Operand | null }
1456
+ // Return from function (null = void return).
1457
+ ```
1458
+
1459
+ ### Basic block and function structure
1460
+
1461
+ ```typescript
1462
+ interface MIRBlock {
1463
+ id: BlockId
1464
+ instrs: MIRInstr[] // non-terminator instructions
1465
+ term: MIRInstr // must be jump | branch | return
1466
+ preds: BlockId[] // predecessor block ids (for dataflow)
1467
+ }
1468
+
1469
+ interface MIRFunction {
1470
+ name: string
1471
+ params: { name: Temp, isMacroParam: boolean }[]
1472
+ blocks: MIRBlock[]
1473
+ entry: BlockId // entry block id (always 'entry')
1474
+ isMacro: boolean // true if any param is a macro param
1475
+ }
1476
+
1477
+ interface MIRModule {
1478
+ functions: MIRFunction[]
1479
+ namespace: string
1480
+ objective: string // scoreboard objective (default: __<namespace>)
1481
+ }
1482
+ ```
1483
+
1484
+ ### Execute subcommands (used in `call_context`)
1485
+
1486
+ ```typescript
1487
+ type ExecuteSubcmd =
1488
+ | { kind: 'as', selector: string } // as @e[tag=foo]
1489
+ | { kind: 'at', selector: string } // at @e[tag=foo]
1490
+ | { kind: 'at_self' } // at @s
1491
+ | { kind: 'positioned', x: string, y: string, z: string }
1492
+ | { kind: 'rotated', yaw: string, pitch: string }
1493
+ | { kind: 'in', dimension: string }
1494
+ | { kind: 'anchored', anchor: 'eyes' | 'feet' }
1495
+ | { kind: 'if_score', a: string, op: CmpOp, b: string }
1496
+ | { kind: 'unless_score', a: string, op: CmpOp, b: string }
1497
+ | { kind: 'if_matches', score: string, range: string }
1498
+ | { kind: 'unless_matches', score: string, range: string }
1499
+ ```
1500
+
1501
+ ---
1502
+
1503
+ ## LIR Instruction Set (Specification)
1504
+
1505
+ LIR is 2-address, MC-specific, typed nodes — no raw strings.
1506
+ Each LIR instruction maps 1:1 (or near) to one MC command.
1507
+
1508
+ ### Slot type
1509
+
1510
+ ```typescript
1511
+ // A scoreboard slot: fake-player name + objective
1512
+ interface Slot { player: string; obj: string }
1513
+ ```
1514
+
1515
+ ### Instructions
1516
+
1517
+ ```typescript
1518
+ type LIRInstr =
1519
+ // ── Scoreboard ───────────────────────────────────────────────────────────
1520
+ | { kind: 'score_set', dst: Slot, value: number }
1521
+ // scoreboard players set <dst.player> <dst.obj> value
1522
+
1523
+ | { kind: 'score_copy', dst: Slot, src: Slot }
1524
+ // scoreboard players operation <dst> = <src>
1525
+
1526
+ | { kind: 'score_add', dst: Slot, src: Slot } // +=
1527
+ | { kind: 'score_sub', dst: Slot, src: Slot } // -=
1528
+ | { kind: 'score_mul', dst: Slot, src: Slot } // *=
1529
+ | { kind: 'score_div', dst: Slot, src: Slot } // /=
1530
+ | { kind: 'score_mod', dst: Slot, src: Slot } // %=
1531
+ | { kind: 'score_min', dst: Slot, src: Slot } // < (min)
1532
+ | { kind: 'score_max', dst: Slot, src: Slot } // > (max)
1533
+ | { kind: 'score_swap', a: Slot, b: Slot } // ><
1534
+
1535
+ // ── Execute store ────────────────────────────────────────────────────────
1536
+ | { kind: 'store_cmd_to_score', dst: Slot, cmd: LIRInstr }
1537
+ // execute store result score <dst> run <cmd>
1538
+
1539
+ | { kind: 'store_score_to_nbt',
1540
+ ns: string, path: string, type: NBTType, scale: number,
1541
+ src: Slot }
1542
+ // execute store result storage <ns> <path> <type> <scale> run scoreboard players get <src>
1543
+
1544
+ | { kind: 'store_nbt_to_score',
1545
+ dst: Slot, ns: string, path: string, scale: number }
1546
+ // execute store result score <dst> run data get storage <ns> <path> <scale>
1547
+
1548
+ // ── NBT ──────────────────────────────────────────────────────────────────
1549
+ | { kind: 'nbt_set_literal', ns: string, path: string, value: string }
1550
+ // data modify storage <ns> <path> set value <value>
1551
+
1552
+ | { kind: 'nbt_copy', srcNs: string, srcPath: string, dstNs: string, dstPath: string }
1553
+ // data modify storage <dstNs> <dstPath> set from storage <srcNs> <srcPath>
1554
+
1555
+ // ── Control flow ─────────────────────────────────────────────────────────
1556
+ | { kind: 'call', fn: string }
1557
+ // function <fn>
1558
+
1559
+ | { kind: 'call_macro', fn: string, storage: string }
1560
+ // function <fn> with storage <storage>
1561
+
1562
+ | { kind: 'call_if_matches', fn: string, slot: Slot, range: string }
1563
+ // execute if score <slot> matches <range> run function <fn>
1564
+
1565
+ | { kind: 'call_unless_matches', fn: string, slot: Slot, range: string }
1566
+
1567
+ | { kind: 'call_if_score',
1568
+ fn: string, a: Slot, op: CmpOp, b: Slot }
1569
+ // execute if score <a> <op> <b> run function <fn>
1570
+
1571
+ | { kind: 'call_unless_score', fn: string, a: Slot, op: CmpOp, b: Slot }
1572
+
1573
+ | { kind: 'call_context', fn: string, subcommands: ExecuteSubcmd[] }
1574
+ // execute [subcommands] run function <fn>
1575
+
1576
+ | { kind: 'return_value', slot: Slot }
1577
+ // scoreboard players operation $ret <obj> = <slot> (then implicit return)
1578
+
1579
+ // ── Macro line ────────────────────────────────────────────────────────────
1580
+ | { kind: 'macro_line', template: string }
1581
+ // A line starting with $ in a macro function.
1582
+ // template uses $(param) substitutions: e.g. "$particle end_rod ^$(px) ^$(py) ^5 ..."
1583
+
1584
+ // ── Arbitrary MC command (for builtins not covered above) ─────────────────
1585
+ | { kind: 'raw', cmd: string }
1586
+ // Emitted verbatim. Use sparingly — prefer typed instructions.
1587
+ ```
1588
+
1589
+ ### LIR function structure
1590
+
1591
+ ```typescript
1592
+ interface LIRFunction {
1593
+ name: string
1594
+ instructions: LIRInstr[] // flat list (no blocks; control flow is via call_if_*)
1595
+ isMacro: boolean
1596
+ macroParams: string[] // names of $(param) substitution keys
1597
+ }
1598
+
1599
+ interface LIRModule {
1600
+ functions: LIRFunction[]
1601
+ namespace: string
1602
+ objective: string
1603
+ }
1604
+ ```
1605
+
1606
+ ---
1607
+
1608
+ ## `execute` Context Blocks in the Pipeline
1609
+
1610
+ `at @s {}`, `as @e[...] {}`, `positioned {}`, etc. are desugared in **Stage 2 (HIR)**.
1611
+
1612
+ ### HIR representation
1613
+
1614
+ ```typescript
1615
+ // In HIR, execute context blocks are a first-class statement:
1616
+ interface HIRExecuteBlock {
1617
+ kind: 'execute_block'
1618
+ subcommands: ExecuteSubcmd[]
1619
+ body: HIRStmt[]
1620
+ }
1621
+ ```
1622
+
1623
+ ### Stage 2 → Stage 3 lowering
1624
+
1625
+ The execute block body is **extracted into a helper function** during HIR → MIR lowering.
1626
+ Variables captured from the enclosing scope are passed as parameters.
1627
+
1628
+ ```redscript
1629
+ // Source
1630
+ as @e[tag=foo] at @s {
1631
+ let dx: int = target_x - my_x;
1632
+ deal_damage(dx);
1633
+ }
1634
+ ```
1635
+
1636
+ HIR:
1637
+ ```
1638
+ execute_block {
1639
+ subcommands: [as @e[tag=foo], at_self],
1640
+ body: [let dx = target_x - my_x, call deal_damage(dx)]
1641
+ }
1642
+ ```
1643
+
1644
+ MIR lowering produces:
1645
+ ```
1646
+ // Capture free variables as args
1647
+ call_context '_exec_helper_0', [as @e[tag=foo], at_self]
1648
+
1649
+ // New MIR function:
1650
+ fn _exec_helper_0(target_x: int, my_x: int):
1651
+ t0 = sub target_x, my_x
1652
+ call deal_damage [t0]
1653
+ return void
1654
+ ```
1655
+
1656
+ In LIR:
1657
+ ```
1658
+ call_context 'ns:_exec_helper_0' [as @e[tag=foo], at @s]
1659
+ ```
1660
+
1661
+ Emitted mcfunction:
1662
+ ```
1663
+ execute as @e[tag=foo] at @s run function ns:_exec_helper_0
1664
+ ```
1665
+
1666
+ ### Nested execute blocks
1667
+
1668
+ Nested `as ... at ... { as ... { } }` blocks become nested helper functions.
1669
+ The compiler generates unique names (`_exec_0`, `_exec_1`, ...) in the order
1670
+ they appear in the source.
1671
+
1672
+ ---
1673
+
1674
+ ## Migration Strategy: Implementing the New Compiler
1675
+
1676
+ ### Approach: parallel implementation on a feature branch
1677
+
1678
+ Do **not** refactor in-place. The current compiler is working and serving users.
1679
+ Implement the new pipeline in a separate branch (`refactor/pipeline-v2`) until
1680
+ it passes 920/920 tests, then merge and delete the old code.
1681
+
1682
+ ### Directory structure during migration
1683
+
1684
+ ```
1685
+ src/ (current compiler — untouched during refactor)
1686
+ src/ (new compiler — staged implementation)
1687
+ parser/ → copy of existing src/parser/ (Stage 1, keep as-is)
1688
+ lexer/ → copy of existing src/lexer/ (Stage 1, keep as-is)
1689
+ ast/ → copy of existing src/ast/ types
1690
+ hir/ → Stage 2: HIR types + AST→HIR lowering
1691
+ mir/ → Stage 3: MIR types + HIR→MIR lowering
1692
+ optimizer/ → Stage 4: MIR passes (new, 3-address aware)
1693
+ lir/ → Stage 5+6: LIR types + MIR→LIR + LIR passes
1694
+ emit/ → Stage 7: LIR → .mcfunction
1695
+ compile.ts → top-level entry point
1696
+ cli.ts → wire up to existing CLI
1697
+ ```
1698
+
1699
+ ### Progressive test coverage: lights up as stages complete
1700
+
1701
+ The 920 e2e tests only reach 920/920 when the full pipeline (all 7 stages) is
1702
+ complete. Do not expect them to be green mid-refactor. Each stage has its own
1703
+ appropriate test criterion:
1704
+
1705
+ | Stage | What to test | 920 e2e? |
1706
+ |---|---|---|
1707
+ | Stage 1 (parser/lexer) | Existing parser unit tests | irrelevant (no codegen) |
1708
+ | Stage 2 (HIR) | HIR unit tests: check desugaring of for/ternary/+= etc. Verify HIR is well-formed. | ✗ |
1709
+ | Stage 3 (MIR) | MIR unit tests: known patterns produce expected 3-address sequences. MIR verifier passes. | ✗ |
1710
+ | Stage 4 (optimizer) | Optimizer unit tests: input MIR → expected output MIR for each pass. | ✗ |
1711
+ | Stage 5 (LIR) | LIR unit tests: MIR→LIR lowering of specific patterns. Simple programs start producing valid mcfunction. | partial |
1712
+ | Stage 6 (LIR opt) | More programs produce correct output. Count improves vs Stage 5 baseline. | growing |
1713
+ | Stage 7 (emit) | 920/920 e2e tests pass. ✅ | ✅ |
1714
+
1715
+ **Rule:** write and pass the unit tests for a stage before moving to the next.
1716
+ Adapt as needed — the design is a guide, not a contract. If a stage boundary
1717
+ turns out to be wrong, merge stages or split them. The goal is green tests and
1718
+ maintainable code, not architectural purity.
1719
+
1720
+ ### Test harness
1721
+
1722
+ The existing 920 tests use `MCRuntime` and test final behavior, not IR internals.
1723
+ They are the primary regression suite and must pass at every stage gate.
1724
+
1725
+ Add **IR-level unit tests** for stages 2–6 independently:
1726
+ - HIR tests: check AST→HIR desugaring (for→while, ternary expansion, etc.)
1727
+ - MIR tests: check instruction sequences for known patterns
1728
+ - Optimizer tests: input MIR → expected output MIR for each pass
1729
+ - LIR tests: check MIR→LIR lowering of specific instruction patterns
1730
+
1731
+ These new tests live in `src/__tests__/`.
1732
+
1733
+ ### What to tell Claude
1734
+
1735
+ When handing this to Claude for implementation:
1736
+ 1. Provide this document as context
1737
+ 2. Implement one stage at a time, in order (1 → 2 → 3 → 4 → 5 → 6 → 7)
1738
+ 3. Write the verifier for each stage before the next stage begins
1739
+ 4. Keep `npm test` (920 tests) passing at every stage gate
1740
+ 5. Commit after each stage gate passes
1741
+ 6. Do not start Stage N+1 until Stage N gate is green
1742
+
1743
+
1744
+ ---
1745
+
1746
+ ## Standard Library Redesign
1747
+
1748
+ The current stdlib was built incrementally and has several design problems.
1749
+ The refactor is an opportunity to fix them properly.
1750
+
1751
+ ### Current problems
1752
+
1753
+ **1. Naming inconsistency**
1754
+
1755
+ ```
1756
+ sin_fixed cos_fixed sqrt_fixed ← _fixed suffix
1757
+ mulfix divfix ← no underscore, no _fixed
1758
+ lerp clamp ← takes int but semantics are fixed-point
1759
+ ```
1760
+
1761
+ No unified convention. A user reading code cannot tell which functions operate
1762
+ on fixed-point values without reading the docs.
1763
+
1764
+ **2. No type enforcement for fixed-point values**
1765
+
1766
+ Every fixed-point function takes and returns `int`. There is nothing stopping
1767
+ a user from passing a raw integer (e.g. pixel coordinate `px = 10`) to
1768
+ `mulfix` where a fixed-point value (e.g. `sin_fixed(45) = 707`) was expected.
1769
+ The compiler cannot catch this. Silent wrong-answer bugs.
1770
+
1771
+ **3. `mulfix` overflow**
1772
+
1773
+ ```redscript
1774
+ fn mulfix(a: int, b: int) -> int { return a * b / 1000; }
1775
+ ```
1776
+
1777
+ `a * b` is a scoreboard `*=` operation, which wraps at INT32 (~2.1 billion).
1778
+ If `a = 50000` (= 50.0 in fixed-point) and `b = 50000`, then
1779
+ `a * b = 2,500,000,000` which overflows. Result is silently wrong.
1780
+ Safe range: both inputs < ~46340 (√2³¹ ≈ 46341).
1781
+
1782
+ **4. Vector functions: flat parameters, split return values**
1783
+
1784
+ ```redscript
1785
+ // Current: flat args, two separate functions for one 2D result
1786
+ fn normalize2d_x(x: int, y: int) -> int { ... }
1787
+ fn normalize2d_y(x: int, y: int) -> int { ... }
1788
+ ```
1789
+
1790
+ No struct-based API. Every 2D/3D operation takes 2–6 separate int parameters.
1791
+ `normalize2d_x` / `normalize2d_y` is especially bad — one logical operation
1792
+ split into two functions that each recompute the length independently.
1793
+
1794
+ Internal overflow: `x * 1000000` before dividing by length — overflows if
1795
+ either component > ~2147 (documented but easy to miss).
1796
+
1797
+ **5. `lerp(a, b, t)` mixes int and fixed-point semantics**
1798
+
1799
+ `lerp(0, 100, 500)` = 50. The `t` parameter is fixed-point (500 = 0.5),
1800
+ but `a` and `b` are plain integers. This is logically inconsistent.
1801
+ If everything were `float`, it would just be `lerp(0.0, 100.0, 0.5) = 50.0`.
1802
+
1803
+ ---
1804
+
1805
+ ### Root cause: no `float` type in the old compiler
1806
+
1807
+ All of the above stem from the same source: without a first-class `float` type,
1808
+ there is no way to distinguish `500` (the integer) from `500` (= 0.5 in ×1000
1809
+ fixed-point). The programmer must track the scale mentally. The API carries
1810
+ that burden in naming conventions (`_fixed` suffix) instead of in types.
1811
+
1812
+ ---
1813
+
1814
+ ### New stdlib design: `float` type does the heavy lifting
1815
+
1816
+ With the new type system, `float` = INT32 ×1000 fixed-point, enforced by the
1817
+ compiler. Arithmetic rules:
1818
+
1819
+ | Operation | Left | Right | Result | Codegen |
1820
+ |---|---|---|---|---|
1821
+ | `a + b` | float | float | float | `+=` (no correction) |
1822
+ | `a - b` | float | float | float | `-=` |
1823
+ | `a * b` | float | float | float | `*=` then `/= 1000` (mulfix) |
1824
+ | `a / b` | float | float | float | `*= 1000` then `/=` (divfix) |
1825
+ | `a * n` | float | int | float | `*=` (scale by integer, no correction) |
1826
+ | `a / n` | float | int | float | `/=` |
1827
+ | `int as float` | int → float | — | — | `*= 1000` |
1828
+ | `float as int` | float → int | — | — | `/= 1000` |
1829
+
1830
+ The user never writes `mulfix` or `divfix` — those become `*` and `/` on `float`.
1831
+
1832
+ ---
1833
+
1834
+ ### New stdlib API
1835
+
1836
+ #### math module
1837
+
1838
+ ```redscript
1839
+ // Integer math (unchanged semantics, cleaned up)
1840
+ fn abs(x: int): int
1841
+ fn sign(x: int): int // -1, 0, 1
1842
+ fn min(a: int, b: int): int
1843
+ fn max(a: int, b: int): int
1844
+ fn clamp(x: int, lo: int, hi: int): int
1845
+ fn pow_int(base: int, exp: int): int
1846
+ fn gcd(a: int, b: int): int
1847
+ fn lcm(a: int, b: int): int
1848
+ fn isqrt(n: int): int // integer square root
1849
+
1850
+ // Fixed-point math (now properly typed)
1851
+ fn sin(deg: int): float // was sin_fixed; returns float (×1000)
1852
+ fn cos(deg: int): float // was cos_fixed
1853
+ fn sqrt(x: float): float // was sqrt_fixed; input & output float
1854
+ fn abs_f(x: float): float // float abs
1855
+ fn clamp_f(x: float, lo: float, hi: float): float
1856
+ fn lerp(a: float, b: float, t: float): float // t in [0.0, 1.0] = [0, 1000]
1857
+ fn map(x: float, in_lo: float, in_hi: float, out_lo: float, out_hi: float): float
1858
+ fn smoothstep(t: float): float // t in [0.0, 1.0], was smoothstep(lo, hi, x)
1859
+ fn smootherstep(t: float): float
1860
+ fn atan2(y: float, x: float): int // returns degrees (int, 0-359)
1861
+ ```
1862
+
1863
+ Removed from public API: `mulfix`, `divfix` — these become `*` and `/` on `float`.
1864
+
1865
+ **`smoothstep` interface change:** `smoothstep(lo, hi, x)` → `smoothstep(t)` where
1866
+ caller normalizes `t` first using `map`. This is simpler and more composable.
1867
+
1868
+ #### vec module (struct-based)
1869
+
1870
+ ```redscript
1871
+ struct Vec2 { x: float; y: float; }
1872
+ struct Vec3 { x: float; y: float; z: float; }
1873
+
1874
+ impl Vec2 {
1875
+ fn add(self, other: Vec2): Vec2
1876
+ fn sub(self, other: Vec2): Vec2
1877
+ fn scale(self, s: float): Vec2 // scalar multiply
1878
+ fn dot(self, other: Vec2): float
1879
+ fn length_sq(self): float // x*x + y*y (no sqrt, no overflow)
1880
+ fn length(self): float // sqrt of length_sq
1881
+ fn dist(self, other: Vec2): float
1882
+ fn manhattan(self, other: Vec2): float
1883
+ fn chebyshev(self, other: Vec2): float
1884
+ fn lerp(self, other: Vec2, t: float): Vec2
1885
+ }
1886
+ ```
1887
+
1888
+ **Multi-value return problem:** `normalize()` must return a `Vec2` (two values),
1889
+ but MC functions return one INT32. Options:
1890
+
1891
+ 1. **Two functions:** `fn normalize_x(self): float` + `fn normalize_y(self): float`
1892
+ (current approach — ugly but simple)
1893
+
1894
+ 2. **Write to output parameter via well-known storage:**
1895
+ ```redscript
1896
+ fn normalize(self): Vec2
1897
+ // compiler generates: compute into $ret_x / $ret_y slots
1898
+ // call site: reads those slots back into the dest Vec2
1899
+ ```
1900
+ Requires compiler support for multi-slot struct return values.
1901
+
1902
+ 3. **Explicit output parameter:**
1903
+ ```redscript
1904
+ fn normalize(self, out_x: int, out_y: int) // caller passes NBT paths
1905
+ ```
1906
+ Awkward in user code.
1907
+
1908
+ **Decision: implement option 2 for the new compiler.** Struct return values
1909
+ use per-field "return slots" (`$ret_<field> __ns`). The caller reads them after
1910
+ the call. The compiler generates this transparently.
1911
+
1912
+ #### timer module (multi-instance)
1913
+
1914
+ ```redscript
1915
+ struct Timer { _id: int; }
1916
+
1917
+ impl Timer {
1918
+ @static fn new(): Timer // allocates a slot, returns id
1919
+ fn start(self, ticks: int) // begin countdown
1920
+ fn tick(self) // call in @tick — decrements if active
1921
+ fn is_done(self): bool
1922
+ fn cancel(self)
1923
+ }
1924
+ ```
1925
+
1926
+ Uses `_id`-scoped fake player names: `timer_0_ticks`, `timer_1_ticks`, etc.
1927
+ Breaks the single-instance limitation.
1928
+
1929
+ #### bigint module (unchanged API, internal cleanup)
1930
+
1931
+ BigInt API is already reasonable. Internal cleanup:
1932
+ - Use struct: `struct BigInt { _id: int }`
1933
+ - Slot-based NBT storage (already uses `rs:bigint`, extend to per-instance paths)
1934
+ - Add: `shift_left`, `mod_pow`, string conversion
1935
+
1936
+ ---
1937
+
1938
+ ### Overflow: permanent constraints
1939
+
1940
+ These cannot be eliminated — they are fundamental to INT32 scoreboard arithmetic.
1941
+
1942
+ | Operation | Safe range | Notes |
1943
+ |---|---|---|
1944
+ | `a * b` (int × int) | both < 46341 | INT32 overflow at 46341² |
1945
+ | `float * float` | both < 46.341 (= 46341 fixed-point) | same limit |
1946
+ | `normalize2d` | input components < ~2147 | intermediate `x * 1000000` |
1947
+ | `sin(deg) * r` (float × float) | r < 46341 | sin output ≤ 1000 |
1948
+
1949
+ The stdlib should document these clearly and provide overflow-safe variants
1950
+ (e.g. `length_sq` instead of `length` when the square is sufficient).
1951
+
1952
+
1953
+ ---
1954
+
1955
+ ## Testing During the Refactor (Addendum)
1956
+
1957
+ ### Write tests alongside implementation, not after
1958
+
1959
+ Each stage should produce its own tests as it is built.
1960
+ Do not batch all testing to the end.
1961
+
1962
+ ```
1963
+ Implement Stage N → write unit tests for Stage N → tests pass → commit → Stage N+1
1964
+ ```
1965
+
1966
+ Stage-local tests are small and focused:
1967
+ - HIR tests: "this source snippet produces this HIR node"
1968
+ - MIR tests: "this HIR function produces this 3-address sequence"
1969
+ - Optimizer tests: "this MIR input becomes this MIR output after pass X"
1970
+ - LIR tests: "this MIR instruction lowers to this LIR instruction"
1971
+
1972
+ The 920 e2e tests are a final integration check, not a substitute for unit tests.
1973
+
1974
+ ### Syntax changes will break many existing tests — that's fine
1975
+
1976
+ The new compiler introduces syntax changes that are not backward-compatible
1977
+ with the current `.mcrs` test fixtures. This is expected and acceptable.
1978
+
1979
+ Known breaking changes:
1980
+
1981
+ | Old syntax | New syntax |
1982
+ |---|---|
1983
+ | `fn foo(x: int) -> int` | `fn foo(x: int): int` |
1984
+ | `@keep fn foo()` | `export fn foo()` |
1985
+ | `struct Foo` + `impl Foo` (separate) | `struct Foo { ... impl ... }` or keep separate — TBD |
1986
+ | `use "path"` (current) | may stay the same |
1987
+ | `float` type was mostly `int` in practice | `float` is now distinct |
1988
+
1989
+ **Strategy for existing tests:**
1990
+ - Tests whose source syntax changes must be updated before they can run against the new compiler
1991
+ - Do not force the new compiler to accept old syntax for test compatibility
1992
+ - Update test fixtures as part of Stage 7 (when the full pipeline is wired up and tests are being migrated)
1993
+ - The old compiler in `src/` still exists until the refactor is complete — old tests can still run against it at any time
1994
+
1995
+ ### Test structure for the new compiler (`src/__tests__/`)
1996
+
1997
+ ```
1998
+ src/__tests__/
1999
+ hir/
2000
+ desugar.test.ts for → while transforms, ternary expansion, etc.
2001
+ execute-blocks.test.ts execute context block lowering to helper fns
2002
+ mir/
2003
+ arithmetic.test.ts int/float arithmetic sequences
2004
+ control-flow.test.ts if/while/loop → CFG branch structure
2005
+ calls.test.ts regular + macro function calls
2006
+ verify.test.ts MIR verifier catches malformed IR
2007
+ optimizer/
2008
+ constant-fold.test.ts
2009
+ copy-prop.test.ts
2010
+ dce.test.ts
2011
+ block-merge.test.ts
2012
+ lir/
2013
+ scoreboard.test.ts score_copy/add/etc lowering
2014
+ execute.test.ts call_if_matches, call_context lowering
2015
+ macro.test.ts macro_line generation
2016
+ e2e/
2017
+ *.test.ts updated versions of the 920 tests (staged migration)
2018
+ ```
2019
+
2020
+
2021
+ ---
2022
+
2023
+ ## MCRuntime Profiler
2024
+
2025
+ ### Motivation
2026
+
2027
+ `MCRuntime` already simulates full MC execution (scoreboard, NBT, function dispatch).
2028
+ Adding a profiling mode costs almost nothing and unlocks:
2029
+
2030
+ 1. **Stdlib tuning** — find the optimal lookup table resolution for trig functions
2031
+ 2. **Error analysis** — measure fixed-point accuracy vs JS float ground truth
2032
+ 3. **Coroutine BATCH calibration** — auto-estimate safe batch size from per-iteration command count
2033
+ 4. **Regression detection** — alert when a refactor increases command counts
2034
+
2035
+ ### Implementation
2036
+
2037
+ ```typescript
2038
+ interface ProfilingOptions {
2039
+ enabled: boolean
2040
+ trackPerFunction: boolean // per-function command counts
2041
+ groundTruth?: boolean // compare int results to JS float (for math functions)
2042
+ }
2043
+
2044
+ class MCRuntime {
2045
+ // existing: scoreboard, storage, function dispatch...
2046
+
2047
+ // profiling (opt-in, zero cost when disabled)
2048
+ private profiling?: ProfilingOptions
2049
+ commandCount = 0
2050
+ functionStats = new Map<string, { calls: number, totalCmds: number, maxDepth: number }>()
2051
+
2052
+ constructor(options?: { profiling?: ProfilingOptions }) { ... }
2053
+ }
2054
+ ```
2055
+
2056
+ Usage:
2057
+ ```typescript
2058
+ const rt = new MCRuntime({ profiling: { enabled: true, trackPerFunction: true } })
2059
+ rt.call('rsdemo:_draw', { px: 500, py: 707 })
2060
+ console.log(rt.functionStats)
2061
+ // Map {
2062
+ // 'rsdemo:_draw' → { calls: 1, totalCmds: 3, maxDepth: 1 }
2063
+ // }
2064
+ console.log(rt.commandCount) // 3
2065
+ ```
2066
+
2067
+ ### Error analysis for math functions
2068
+
2069
+ Compare MCRuntime output (integer fixed-point) against JS `Math` (IEEE 754 float):
2070
+
2071
+ ```typescript
2072
+ function analyzeError(fn: string, inputRange: number[], expected: (x: number) => number) {
2073
+ const rt = new MCRuntime()
2074
+ const errors: number[] = []
2075
+
2076
+ for (const x of inputRange) {
2077
+ const computed = rt.call(fn, x) // integer ×1000 result
2078
+ const truth = expected(x) * 1000 // JS float × 1000
2079
+ errors.push(Math.abs(computed - truth))
2080
+ }
2081
+
2082
+ return {
2083
+ maxError: Math.max(...errors),
2084
+ avgError: errors.reduce((a,b) => a+b) / errors.length,
2085
+ worstCase: inputRange[errors.indexOf(Math.max(...errors))],
2086
+ }
2087
+ }
2088
+
2089
+ // sin lookup table (91 entries, 1° resolution):
2090
+ analyzeError('math:sin', range(0, 360), x => Math.sin(x * Math.PI / 180))
2091
+ // → { maxError: 0.89, avgError: 0.31, worstCase: 1 }
2092
+
2093
+ // If we doubled to 181 entries (0.5° resolution), how much better?
2094
+ // → { maxError: 0.22, avgError: 0.08 } — but +90 storage entries, +~20 commands per call
2095
+ // Trade-off is now measurable, not guessed.
2096
+ ```
2097
+
2098
+ This makes stdlib decisions data-driven:
2099
+
2100
+ | Table size | Max error | Avg error | Commands/call |
2101
+ |---|---|---|---|
2102
+ | 91 entries (1°) | 0.89 | 0.31 | ~12 |
2103
+ | 181 entries (0.5°) | 0.22 | 0.08 | ~14 |
2104
+ | 361 entries (0.25°) | 0.055 | 0.02 | ~16 |
2105
+
2106
+ Pick the row that fits your application's accuracy budget.
2107
+
2108
+ ### Coroutine BATCH calibration
2109
+
2110
+ The profiler directly answers "how big can my BATCH be without exceeding 65536?":
2111
+
2112
+ ```typescript
2113
+ const rt = new MCRuntime({ profiling: { enabled: true } })
2114
+
2115
+ // Profile one iteration of the loop body
2116
+ rt.call('mymod:_loop_body_once', { i: 0 })
2117
+ const cmdsPerIter = rt.commandCount
2118
+
2119
+ // Safe BATCH = floor(budget / cmdsPerIter)
2120
+ const SAFE_BATCH = Math.floor(65536 / cmdsPerIter)
2121
+ console.log(`Recommended @coroutine(batch=${SAFE_BATCH})`)
2122
+ ```
2123
+
2124
+ The compiler can run this automatically during compilation when `@coroutine`
2125
+ is used without an explicit `batch=N` — profile the loop body in MCRuntime,
2126
+ compute the safe batch, insert it.
2127
+
2128
+ ### Placement
2129
+
2130
+ Profiling mode lives in `src/runtime/index.ts` (or `src/runtime/` now).
2131
+ It is off by default and adds zero overhead to the 920 e2e tests.
2132
+
2133
+ New test file: `src/__tests__/profiler/stdlib-accuracy.test.ts`
2134
+ Runs the error analysis for all math functions and asserts max error < threshold.
2135
+ This becomes a regression test: if a stdlib change degrades accuracy, the test fails.
2136
+
2137
+
2138
+ ---
2139
+
2140
+ ## Compile-Time Macros (`@comptime`)
2141
+
2142
+ ### Motivation
2143
+
2144
+ Lookup tables (sin, cos, atan2, noise permutation, easing curves) are currently
2145
+ generated by external Python scripts and pasted as literals into `.mcrs` files.
2146
+ Changing table resolution requires re-running the script. `@comptime` eliminates
2147
+ this friction.
2148
+
2149
+ ### Semantics
2150
+
2151
+ A function annotated `@comptime` runs **in the compiler** (TypeScript/JS runtime)
2152
+ rather than in Minecraft. It has access to exact floating-point math but no MC
2153
+ builtins. Its return value is embedded as a compile-time literal.
2154
+
2155
+ ```redscript
2156
+ // In stdlib/math.mcrs:
2157
+ @comptime fn _sin_table_data(resolution: int): int[] {
2158
+ // Runs in compiler — uses JS Math.sin (exact float)
2159
+ let table: int[] = [];
2160
+ for (let i = 0; i <= resolution; i++) {
2161
+ let deg = i as float * 90.0 / resolution as float;
2162
+ table.push(floor(Math.sin(deg * PI / 180.0) * 1000.0));
2163
+ }
2164
+ return table;
2165
+ }
2166
+
2167
+ // User-configurable resolution (compile-time constant):
2168
+ const SIN_RESOLUTION: comptime int = 91; // override in your datapack
2169
+
2170
+ @load fn _math_init() {
2171
+ // Compiler replaces this call with the literal array at compile time
2172
+ storage_set_array("math:tables", "sin", _sin_table_data(SIN_RESOLUTION));
2173
+ }
2174
+ ```
2175
+
2176
+ The emitted `_math_init.mcfunction` contains a single command with the literal
2177
+ array — zero runtime overhead.
2178
+
2179
+ ### Restrictions on `@comptime` functions
2180
+
2181
+ - No MC builtins (`particle`, `kill`, `scoreboard`, etc.)
2182
+ - No `@tick` / `@load` / `@keep` / `export`
2183
+ - No side effects (pure function only)
2184
+ - Arguments must be compile-time constants (literals or `comptime` variables)
2185
+ - Return types: `int`, `float`, `bool`, `string`, or arrays of these
2186
+
2187
+ ### Implementation
2188
+
2189
+ The compiler evaluates `@comptime` calls using a small tree-walking interpreter
2190
+ (~300–400 lines) that handles: arithmetic, boolean logic, array literals/push,
2191
+ for/while loops, if/else, and a whitelist of math builtins (`Math.sin`,
2192
+ `Math.cos`, `Math.sqrt`, `Math.floor`, `Math.ceil`, `Math.round`, `PI`, `E`).
2193
+
2194
+ This runs during Stage 2 (HIR lowering) — comptime calls are evaluated and
2195
+ replaced with their results before the HIR is constructed.
2196
+
2197
+ ### User-facing benefits
2198
+
2199
+ ```redscript
2200
+ // Tune sin table resolution for your use case:
2201
+ const SIN_RESOLUTION: comptime int = 181; // 0.5° resolution
2202
+
2203
+ // Generate a custom easing curve (Bezier-sampled, 64 points):
2204
+ @comptime fn _ease_out_cubic(n: int): int[] {
2205
+ let table: int[] = [];
2206
+ for (let i = 0; i < n; i++) {
2207
+ let t = i as float / n as float;
2208
+ let v = 1.0 - (1.0 - t) * (1.0 - t) * (1.0 - t);
2209
+ table.push(floor(v * 1000.0));
2210
+ }
2211
+ return table;
2212
+ }
2213
+ const EASE_TABLE: int[] = _ease_out_cubic(64);
2214
+ ```
2215
+
2216
+ ---
2217
+
2218
+ ## Safe Math: Default On
2219
+
2220
+ ### The `safe-math` mode
2221
+
2222
+ All `float * float` and `float / float` operations default to overflow-safe
2223
+ variants. This is the **default** in the new compiler; there is no flag to
2224
+ disable it (unlike the old `--no-dce`).
2225
+
2226
+ **Safe multiply** (`a * b` where both are `float`):
2227
+ ```
2228
+ # Fast (current, can overflow if |a| or |b| > 46341 in fixed-point):
2229
+ scoreboard players operation $dst __ns = $a __ns
2230
+ scoreboard players operation $dst __ns *= $b __ns
2231
+ scoreboard players operation $dst __ns /= 1000
2232
+
2233
+ # Safe default (costs ~4 extra commands, prevents overflow up to ±1,000,000):
2234
+ # Divide both by 32 before multiplying, then compensate
2235
+ scoreboard players operation $t __ns = $a __ns
2236
+ scoreboard players operation $t __ns /= 32
2237
+ scoreboard players operation $u __ns = $b __ns
2238
+ scoreboard players operation $u __ns /= 32
2239
+ scoreboard players operation $dst __ns = $t __ns
2240
+ scoreboard players operation $dst __ns *= $u __ns
2241
+ scoreboard players operation $dst __ns *= 1000
2242
+ scoreboard players operation $dst __ns /= 1024 # 32*32/1000 ≈ 1.024 correction
2243
+ ```
2244
+
2245
+ This extends the safe multiplication range from ±46,341 to approximately ±1,000,000
2246
+ in fixed-point (= ±1,000 real units), sufficient for all typical MC use cases.
2247
+
2248
+ ### Functions that always use safe logic (regardless of mode)
2249
+
2250
+ - `normalize2d` / `normalize3d` — internal `x * 1_000_000` has a ±2147 limit;
2251
+ reimplemented to avoid the intermediate, always safe
2252
+ - `length2d` / `length3d` — internal `x * x` safe up to ±46,341 (= ±46.341 units);
2253
+ acceptable for most MC distances; document the constraint
2254
+
2255
+ ### Profiler-verified: safe math adds ~4 commands per float multiply
2256
+
2257
+ The profiler can measure the exact command-count cost of safe vs fast math.
2258
+ Default is safe-on; users who need maximum performance and know their operand
2259
+ ranges can use explicit integer arithmetic instead of `float`.
2260
+