redscript-mc 1.0.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 (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/publish-extension.yml +35 -0
  7. package/LICENSE +21 -0
  8. package/README.md +261 -0
  9. package/README.zh.md +261 -0
  10. package/dist/__tests__/cli.test.d.ts +1 -0
  11. package/dist/__tests__/cli.test.js +140 -0
  12. package/dist/__tests__/codegen.test.d.ts +1 -0
  13. package/dist/__tests__/codegen.test.js +121 -0
  14. package/dist/__tests__/diagnostics.test.d.ts +4 -0
  15. package/dist/__tests__/diagnostics.test.js +149 -0
  16. package/dist/__tests__/e2e.test.d.ts +6 -0
  17. package/dist/__tests__/e2e.test.js +1528 -0
  18. package/dist/__tests__/lexer.test.d.ts +1 -0
  19. package/dist/__tests__/lexer.test.js +316 -0
  20. package/dist/__tests__/lowering.test.d.ts +1 -0
  21. package/dist/__tests__/lowering.test.js +819 -0
  22. package/dist/__tests__/mc-integration.test.d.ts +12 -0
  23. package/dist/__tests__/mc-integration.test.js +395 -0
  24. package/dist/__tests__/mc-syntax.test.d.ts +1 -0
  25. package/dist/__tests__/mc-syntax.test.js +112 -0
  26. package/dist/__tests__/nbt.test.d.ts +1 -0
  27. package/dist/__tests__/nbt.test.js +82 -0
  28. package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
  29. package/dist/__tests__/optimizer-advanced.test.js +124 -0
  30. package/dist/__tests__/optimizer.test.d.ts +1 -0
  31. package/dist/__tests__/optimizer.test.js +118 -0
  32. package/dist/__tests__/parser.test.d.ts +1 -0
  33. package/dist/__tests__/parser.test.js +717 -0
  34. package/dist/__tests__/repl.test.d.ts +1 -0
  35. package/dist/__tests__/repl.test.js +27 -0
  36. package/dist/__tests__/runtime.test.d.ts +1 -0
  37. package/dist/__tests__/runtime.test.js +276 -0
  38. package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
  39. package/dist/__tests__/structure-optimizer.test.js +33 -0
  40. package/dist/__tests__/typechecker.test.d.ts +1 -0
  41. package/dist/__tests__/typechecker.test.js +364 -0
  42. package/dist/ast/types.d.ts +357 -0
  43. package/dist/ast/types.js +9 -0
  44. package/dist/cli.d.ts +11 -0
  45. package/dist/cli.js +407 -0
  46. package/dist/codegen/cmdblock/index.d.ts +26 -0
  47. package/dist/codegen/cmdblock/index.js +45 -0
  48. package/dist/codegen/mcfunction/index.d.ts +34 -0
  49. package/dist/codegen/mcfunction/index.js +413 -0
  50. package/dist/codegen/structure/index.d.ts +18 -0
  51. package/dist/codegen/structure/index.js +249 -0
  52. package/dist/compile.d.ts +30 -0
  53. package/dist/compile.js +152 -0
  54. package/dist/data/arena/function/__load.mcfunction +6 -0
  55. package/dist/data/arena/function/__tick.mcfunction +2 -0
  56. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  57. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  58. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  59. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  60. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  61. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  62. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  63. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  64. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  65. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  66. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  67. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  68. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  69. package/dist/data/counter/function/__load.mcfunction +5 -0
  70. package/dist/data/counter/function/__tick.mcfunction +2 -0
  71. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  72. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  73. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  74. package/dist/data/minecraft/tags/function/load.json +5 -0
  75. package/dist/data/minecraft/tags/function/tick.json +5 -0
  76. package/dist/data/quiz/function/__load.mcfunction +16 -0
  77. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  78. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  79. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  80. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  81. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  82. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  83. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  84. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  85. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  86. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  87. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  88. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  89. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  90. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  91. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  92. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  93. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  94. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  95. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  96. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  97. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  98. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  99. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  100. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  101. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  102. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  103. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  104. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  105. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  106. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  107. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  108. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  109. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  110. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  111. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  112. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  113. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  114. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  115. package/dist/data/shop/function/__load.mcfunction +7 -0
  116. package/dist/data/shop/function/__tick.mcfunction +3 -0
  117. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  118. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  119. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  120. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  121. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  122. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  123. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  124. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  125. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  126. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  127. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  128. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  129. package/dist/data/turret/function/__load.mcfunction +5 -0
  130. package/dist/data/turret/function/__tick.mcfunction +4 -0
  131. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  132. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  133. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  134. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  135. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  136. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  137. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  138. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  139. package/dist/diagnostics/index.d.ts +44 -0
  140. package/dist/diagnostics/index.js +140 -0
  141. package/dist/index.d.ts +53 -0
  142. package/dist/index.js +126 -0
  143. package/dist/ir/builder.d.ts +32 -0
  144. package/dist/ir/builder.js +99 -0
  145. package/dist/ir/types.d.ts +117 -0
  146. package/dist/ir/types.js +15 -0
  147. package/dist/lexer/index.d.ts +36 -0
  148. package/dist/lexer/index.js +458 -0
  149. package/dist/lowering/index.d.ts +106 -0
  150. package/dist/lowering/index.js +2041 -0
  151. package/dist/mc-test/client.d.ts +128 -0
  152. package/dist/mc-test/client.js +174 -0
  153. package/dist/mc-test/runner.d.ts +28 -0
  154. package/dist/mc-test/runner.js +150 -0
  155. package/dist/mc-test/setup.d.ts +11 -0
  156. package/dist/mc-test/setup.js +98 -0
  157. package/dist/mc-validator/index.d.ts +17 -0
  158. package/dist/mc-validator/index.js +322 -0
  159. package/dist/nbt/index.d.ts +86 -0
  160. package/dist/nbt/index.js +250 -0
  161. package/dist/optimizer/commands.d.ts +36 -0
  162. package/dist/optimizer/commands.js +349 -0
  163. package/dist/optimizer/passes.d.ts +34 -0
  164. package/dist/optimizer/passes.js +227 -0
  165. package/dist/optimizer/structure.d.ts +8 -0
  166. package/dist/optimizer/structure.js +344 -0
  167. package/dist/pack.mcmeta +6 -0
  168. package/dist/parser/index.d.ts +76 -0
  169. package/dist/parser/index.js +1193 -0
  170. package/dist/repl.d.ts +16 -0
  171. package/dist/repl.js +165 -0
  172. package/dist/runtime/index.d.ts +101 -0
  173. package/dist/runtime/index.js +1288 -0
  174. package/dist/typechecker/index.d.ts +42 -0
  175. package/dist/typechecker/index.js +629 -0
  176. package/docs/COMPILATION_STATS.md +142 -0
  177. package/docs/IMPLEMENTATION_GUIDE.md +512 -0
  178. package/docs/LANGUAGE_REFERENCE.md +415 -0
  179. package/docs/MC_MAPPING.md +280 -0
  180. package/docs/STRUCTURE_TARGET.md +80 -0
  181. package/docs/mc-reference/commands.md +259 -0
  182. package/editors/vscode/.vscodeignore +10 -0
  183. package/editors/vscode/LICENSE +21 -0
  184. package/editors/vscode/README.md +78 -0
  185. package/editors/vscode/build.mjs +28 -0
  186. package/editors/vscode/icon.png +0 -0
  187. package/editors/vscode/mcfunction-language-configuration.json +28 -0
  188. package/editors/vscode/out/extension.js +7236 -0
  189. package/editors/vscode/package-lock.json +566 -0
  190. package/editors/vscode/package.json +137 -0
  191. package/editors/vscode/redscript-language-configuration.json +28 -0
  192. package/editors/vscode/snippets/redscript.json +114 -0
  193. package/editors/vscode/src/codeactions.ts +89 -0
  194. package/editors/vscode/src/completion.ts +130 -0
  195. package/editors/vscode/src/extension.ts +239 -0
  196. package/editors/vscode/src/hover.ts +1120 -0
  197. package/editors/vscode/src/symbols.ts +207 -0
  198. package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
  199. package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
  200. package/editors/vscode/tsconfig.json +13 -0
  201. package/jest.config.js +5 -0
  202. package/package.json +38 -0
  203. package/src/__tests__/cli.test.ts +130 -0
  204. package/src/__tests__/codegen.test.ts +128 -0
  205. package/src/__tests__/diagnostics.test.ts +195 -0
  206. package/src/__tests__/e2e.test.ts +1721 -0
  207. package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
  208. package/src/__tests__/formatter.test.ts +46 -0
  209. package/src/__tests__/lexer.test.ts +356 -0
  210. package/src/__tests__/lowering.test.ts +962 -0
  211. package/src/__tests__/mc-integration.test.ts +409 -0
  212. package/src/__tests__/mc-syntax.test.ts +96 -0
  213. package/src/__tests__/nbt.test.ts +58 -0
  214. package/src/__tests__/optimizer-advanced.test.ts +144 -0
  215. package/src/__tests__/optimizer.test.ts +129 -0
  216. package/src/__tests__/parser.test.ts +800 -0
  217. package/src/__tests__/repl.test.ts +33 -0
  218. package/src/__tests__/runtime.test.ts +289 -0
  219. package/src/__tests__/structure-optimizer.test.ts +38 -0
  220. package/src/__tests__/typechecker.test.ts +395 -0
  221. package/src/ast/types.ts +248 -0
  222. package/src/cli.ts +445 -0
  223. package/src/codegen/cmdblock/index.ts +63 -0
  224. package/src/codegen/mcfunction/index.ts +471 -0
  225. package/src/codegen/structure/index.ts +305 -0
  226. package/src/compile.ts +188 -0
  227. package/src/diagnostics/index.ts +186 -0
  228. package/src/examples/README.md +77 -0
  229. package/src/examples/SHOWCASE_GAME.md +43 -0
  230. package/src/examples/arena.rs +44 -0
  231. package/src/examples/counter.rs +12 -0
  232. package/src/examples/pvp_arena.rs +131 -0
  233. package/src/examples/quiz.rs +90 -0
  234. package/src/examples/rpg.rs +13 -0
  235. package/src/examples/shop.rs +30 -0
  236. package/src/examples/showcase_game.rs +552 -0
  237. package/src/examples/stdlib_demo.rs +181 -0
  238. package/src/examples/turret.rs +27 -0
  239. package/src/examples/world_manager.rs +23 -0
  240. package/src/formatter/index.ts +22 -0
  241. package/src/index.ts +161 -0
  242. package/src/ir/builder.ts +114 -0
  243. package/src/ir/types.ts +119 -0
  244. package/src/lexer/index.ts +555 -0
  245. package/src/lowering/index.ts +2406 -0
  246. package/src/mc-test/client.ts +259 -0
  247. package/src/mc-test/runner.ts +140 -0
  248. package/src/mc-test/setup.ts +70 -0
  249. package/src/mc-validator/index.ts +367 -0
  250. package/src/nbt/index.ts +321 -0
  251. package/src/optimizer/commands.ts +416 -0
  252. package/src/optimizer/passes.ts +233 -0
  253. package/src/optimizer/structure.ts +441 -0
  254. package/src/parser/index.ts +1437 -0
  255. package/src/repl.ts +165 -0
  256. package/src/runtime/index.ts +1403 -0
  257. package/src/stdlib/README.md +156 -0
  258. package/src/stdlib/combat.rs +20 -0
  259. package/src/stdlib/cooldown.rs +45 -0
  260. package/src/stdlib/math.rs +49 -0
  261. package/src/stdlib/mobs.rs +99 -0
  262. package/src/stdlib/player.rs +29 -0
  263. package/src/stdlib/strings.rs +7 -0
  264. package/src/stdlib/timer.rs +51 -0
  265. package/src/templates/README.md +126 -0
  266. package/src/templates/combat.rs +96 -0
  267. package/src/templates/economy.rs +40 -0
  268. package/src/templates/mini-game-framework.rs +117 -0
  269. package/src/templates/quest.rs +78 -0
  270. package/src/test_programs/zombie_game.rs +25 -0
  271. package/src/typechecker/index.ts +737 -0
  272. package/tsconfig.json +16 -0
@@ -0,0 +1,416 @@
1
+ import type { IRCommand } from '../ir/types'
2
+
3
+ export interface OptimizationStats {
4
+ licmHoists: number
5
+ licmLoopBodies: number
6
+ cseRedundantReads: number
7
+ cseArithmetic: number
8
+ setblockMergedCommands: number
9
+ setblockFillCommands: number
10
+ setblockSavedCommands: number
11
+ deadCodeRemoved: number
12
+ constantFolds: number
13
+ totalCommandsBefore: number
14
+ totalCommandsAfter: number
15
+ }
16
+
17
+ export interface CommandFunction {
18
+ name: string
19
+ commands: IRCommand[]
20
+ }
21
+
22
+ const SCOREBOARD_READ_RE =
23
+ /^execute store result score (\$[A-Za-z0-9_]+) rs run scoreboard players get (\S+) (\S+)$/
24
+ const SCOREBOARD_WRITE_RE =
25
+ /^(?:scoreboard players (?:set|add|remove|reset)\s+(\S+)\s+(\S+)|scoreboard players operation\s+(\S+)\s+(\S+)\s+[+\-*/%]?= )/
26
+ const EXECUTE_STORE_SCORE_RE =
27
+ /^execute store result score (\S+) (\S+) run /
28
+ const FUNCTION_CALL_RE = /^execute as (.+) run function ([^:]+):(.+)$/
29
+ const TEMP_RE = /\$[A-Za-z0-9_]+/g
30
+ const SETBLOCK_RE = /^setblock (-?\d+) (-?\d+) (-?\d+) (\S+)$/
31
+
32
+ export function createEmptyOptimizationStats(): OptimizationStats {
33
+ return {
34
+ licmHoists: 0,
35
+ licmLoopBodies: 0,
36
+ cseRedundantReads: 0,
37
+ cseArithmetic: 0,
38
+ setblockMergedCommands: 0,
39
+ setblockFillCommands: 0,
40
+ setblockSavedCommands: 0,
41
+ deadCodeRemoved: 0,
42
+ constantFolds: 0,
43
+ totalCommandsBefore: 0,
44
+ totalCommandsAfter: 0,
45
+ }
46
+ }
47
+
48
+ function cloneCommand(command: IRCommand): IRCommand {
49
+ return { ...command }
50
+ }
51
+
52
+ function cloneFunctions(functions: CommandFunction[]): CommandFunction[] {
53
+ return functions.map(fn => ({
54
+ name: fn.name,
55
+ commands: fn.commands.map(cloneCommand),
56
+ }))
57
+ }
58
+
59
+ export function mergeOptimizationStats(base: OptimizationStats, delta: Partial<OptimizationStats>): void {
60
+ for (const [key, value] of Object.entries(delta)) {
61
+ base[key as keyof OptimizationStats] += value as number
62
+ }
63
+ }
64
+
65
+ function parseScoreboardWrite(command: string): { player: string; objective: string } | null {
66
+ const executeStoreMatch = command.match(EXECUTE_STORE_SCORE_RE)
67
+ if (executeStoreMatch) {
68
+ return { player: executeStoreMatch[1], objective: executeStoreMatch[2] }
69
+ }
70
+
71
+ const match = command.match(SCOREBOARD_WRITE_RE)
72
+ if (!match) {
73
+ return null
74
+ }
75
+
76
+ if (match[1] && match[2]) {
77
+ return { player: match[1], objective: match[2] }
78
+ }
79
+
80
+ if (match[3] && match[4]) {
81
+ return { player: match[3], objective: match[4] }
82
+ }
83
+
84
+ return null
85
+ }
86
+
87
+ function replaceTemp(command: string, from: string, to: string): string {
88
+ const re = new RegExp(`\\${from}(?![A-Za-z0-9_])`, 'g')
89
+ return command.replace(re, to)
90
+ }
91
+
92
+ function collectObjectiveWrites(functions: CommandFunction[]): Map<string, number> {
93
+ const writes = new Map<string, number>()
94
+
95
+ for (const fn of functions) {
96
+ for (const command of fn.commands) {
97
+ const write = parseScoreboardWrite(command.cmd)
98
+ if (!write) continue
99
+ writes.set(write.objective, (writes.get(write.objective) ?? 0) + 1)
100
+ }
101
+ }
102
+
103
+ return writes
104
+ }
105
+
106
+ function applyLICMInternal(functions: CommandFunction[]): Partial<OptimizationStats> {
107
+ const stats: Partial<OptimizationStats> = { licmHoists: 0, licmLoopBodies: 0 }
108
+ const functionMap = new Map(functions.map(fn => [fn.name, fn]))
109
+ const objectiveWrites = collectObjectiveWrites(functions)
110
+
111
+ for (const fn of functions) {
112
+ const nextCommands: IRCommand[] = []
113
+
114
+ for (const command of fn.commands) {
115
+ const match = command.cmd.match(FUNCTION_CALL_RE)
116
+ if (!match) {
117
+ nextCommands.push(command)
118
+ continue
119
+ }
120
+
121
+ const loopFn = functionMap.get(match[3])
122
+ if (!loopFn) {
123
+ nextCommands.push(command)
124
+ continue
125
+ }
126
+
127
+ const readInfo = new Map<string, { temp: string; player: string; objective: string; uses: number }>()
128
+ const scoreboardWrites = new Set<string>()
129
+
130
+ for (const inner of loopFn.commands) {
131
+ const readMatch = inner.cmd.match(SCOREBOARD_READ_RE)
132
+ if (readMatch) {
133
+ const [, temp, player, objective] = readMatch
134
+ const key = `${player} ${objective}`
135
+ readInfo.set(key, { temp, player, objective, uses: 0 })
136
+ }
137
+
138
+ const write = parseScoreboardWrite(inner.cmd)
139
+ if (write) {
140
+ scoreboardWrites.add(`${write.player} ${write.objective}`)
141
+ }
142
+ }
143
+
144
+ for (const inner of loopFn.commands) {
145
+ for (const info of readInfo.values()) {
146
+ const matches = inner.cmd.match(TEMP_RE) ?? []
147
+ const usageCount = matches.filter(name => name === info.temp).length
148
+ const isDef = inner.cmd.startsWith(`execute store result score ${info.temp} rs run scoreboard players get `)
149
+ if (!isDef) {
150
+ info.uses += usageCount
151
+ }
152
+ }
153
+ }
154
+
155
+ const hoistable = Array.from(readInfo.entries())
156
+ .filter(([key, info]) => {
157
+ if (info.uses < 2) return false
158
+ if ((objectiveWrites.get(info.objective) ?? 0) !== 0) return false
159
+ if (scoreboardWrites.has(key)) return false
160
+ return true
161
+ })
162
+ .map(([, info]) => info)
163
+
164
+ if (hoistable.length === 0) {
165
+ nextCommands.push(command)
166
+ continue
167
+ }
168
+
169
+ const hoistedTemps = new Set(hoistable.map(item => item.temp))
170
+ const rewrittenLoopCommands: IRCommand[] = []
171
+
172
+ for (const inner of loopFn.commands) {
173
+ const readMatch = inner.cmd.match(SCOREBOARD_READ_RE)
174
+ if (readMatch && hoistedTemps.has(readMatch[1])) {
175
+ continue
176
+ }
177
+ rewrittenLoopCommands.push(inner)
178
+ }
179
+
180
+ loopFn.commands = rewrittenLoopCommands
181
+ nextCommands.push(
182
+ ...hoistable.map(item => ({
183
+ cmd: `execute store result score ${item.temp} rs run scoreboard players get ${item.player} ${item.objective}`,
184
+ })),
185
+ command
186
+ )
187
+ stats.licmHoists = (stats.licmHoists ?? 0) + hoistable.length
188
+ stats.licmLoopBodies = (stats.licmLoopBodies ?? 0) + 1
189
+ }
190
+
191
+ fn.commands = nextCommands
192
+ }
193
+
194
+ return stats
195
+ }
196
+
197
+ function extractArithmeticExpression(commands: IRCommand[], index: number): { key: string; dst: string } | null {
198
+ const assign =
199
+ commands[index]?.cmd.match(/^scoreboard players operation (\$[A-Za-z0-9_]+) rs = (\$[A-Za-z0-9_]+|\$const_-?\d+) rs$/) ??
200
+ commands[index]?.cmd.match(/^scoreboard players set (\$[A-Za-z0-9_]+) rs (-?\d+)$/)
201
+ const op = commands[index + 1]?.cmd.match(/^scoreboard players operation (\$[A-Za-z0-9_]+) rs ([+\-*/%]=) (\$[A-Za-z0-9_]+|\$const_-?\d+) rs$/)
202
+ if (!assign || !op || assign[1] !== op[1]) {
203
+ return null
204
+ }
205
+ return {
206
+ key: `${assign[2]} ${op[2]} ${op[3]}`,
207
+ dst: assign[1],
208
+ }
209
+ }
210
+
211
+ function applyCSEInternal(functions: CommandFunction[]): Partial<OptimizationStats> {
212
+ const stats: Partial<OptimizationStats> = { cseRedundantReads: 0, cseArithmetic: 0 }
213
+
214
+ for (const fn of functions) {
215
+ const commands = fn.commands.map(cloneCommand)
216
+ const readCache = new Map<string, string>()
217
+ const exprCache = new Map<string, string>()
218
+ const rewritten: IRCommand[] = []
219
+
220
+ function invalidateByTemp(temp: string): void {
221
+ for (const [key, value] of readCache.entries()) {
222
+ if (value === temp || key.includes(`${temp} `) || key.endsWith(` ${temp}`)) {
223
+ readCache.delete(key)
224
+ }
225
+ }
226
+ for (const [key, value] of exprCache.entries()) {
227
+ if (value === temp || key.includes(temp)) {
228
+ exprCache.delete(key)
229
+ }
230
+ }
231
+ }
232
+
233
+ for (let i = 0; i < commands.length; i++) {
234
+ const command = commands[i]
235
+ const readMatch = command.cmd.match(SCOREBOARD_READ_RE)
236
+ if (readMatch) {
237
+ const [, dst, player, objective] = readMatch
238
+ const key = `${player} ${objective}`
239
+ const cached = readCache.get(key)
240
+ if (cached) {
241
+ stats.cseRedundantReads = (stats.cseRedundantReads ?? 0) + 1
242
+ rewritten.push({ ...command, cmd: `scoreboard players operation ${dst} rs = ${cached} rs` })
243
+ } else {
244
+ readCache.set(key, dst)
245
+ rewritten.push(command)
246
+ }
247
+ invalidateByTemp(dst)
248
+ readCache.set(key, dst)
249
+ continue
250
+ }
251
+
252
+ const expr = extractArithmeticExpression(commands, i)
253
+ if (expr) {
254
+ const cached = exprCache.get(expr.key)
255
+ if (cached) {
256
+ rewritten.push({ ...commands[i], cmd: `scoreboard players operation ${expr.dst} rs = ${cached} rs` })
257
+ stats.cseArithmetic = (stats.cseArithmetic ?? 0) + 1
258
+ i += 1
259
+ } else {
260
+ rewritten.push(command)
261
+ rewritten.push(commands[i + 1])
262
+ exprCache.set(expr.key, expr.dst)
263
+ i += 1
264
+ }
265
+ invalidateByTemp(expr.dst)
266
+ exprCache.set(expr.key, expr.dst)
267
+ continue
268
+ }
269
+
270
+ const write = parseScoreboardWrite(command.cmd)
271
+ if (write) {
272
+ readCache.delete(`${write.player} ${write.objective}`)
273
+ if (write.player.startsWith('$')) {
274
+ invalidateByTemp(write.player)
275
+ }
276
+ }
277
+
278
+ rewritten.push(command)
279
+ }
280
+
281
+ fn.commands = rewritten
282
+ }
283
+
284
+ return stats
285
+ }
286
+
287
+ function batchSetblocksInCommands(commands: IRCommand[]): { commands: IRCommand[]; stats: Partial<OptimizationStats> } {
288
+ const rewritten: IRCommand[] = []
289
+ const stats: Partial<OptimizationStats> = {
290
+ setblockMergedCommands: 0,
291
+ setblockFillCommands: 0,
292
+ setblockSavedCommands: 0,
293
+ }
294
+
295
+ for (let i = 0; i < commands.length; ) {
296
+ const start = commands[i].cmd.match(SETBLOCK_RE)
297
+ if (!start) {
298
+ rewritten.push(commands[i])
299
+ i++
300
+ continue
301
+ }
302
+
303
+ const block = start[4]
304
+ const run = [{ index: i, x: Number(start[1]), y: Number(start[2]), z: Number(start[3]) }]
305
+ let axis: 'x' | 'z' | null = null
306
+ let j = i + 1
307
+
308
+ while (j < commands.length) {
309
+ const next = commands[j].cmd.match(SETBLOCK_RE)
310
+ if (!next || next[4] !== block) break
311
+
312
+ const point = { x: Number(next[1]), y: Number(next[2]), z: Number(next[3]) }
313
+ const prev = run[run.length - 1]
314
+ if (point.y !== prev.y) break
315
+
316
+ const stepX = point.x - prev.x
317
+ const stepZ = point.z - prev.z
318
+ if (axis === null) {
319
+ if (stepX === 1 && stepZ === 0) axis = 'x'
320
+ else if (stepX === 0 && stepZ === 1) axis = 'z'
321
+ else break
322
+ }
323
+
324
+ const valid = axis === 'x'
325
+ ? point.z === prev.z && stepX === 1 && stepZ === 0
326
+ : point.x === prev.x && stepX === 0 && stepZ === 1
327
+ if (!valid) break
328
+
329
+ run.push({ index: j, ...point })
330
+ j++
331
+ }
332
+
333
+ if (run.length >= 2) {
334
+ const first = run[0]
335
+ const last = run[run.length - 1]
336
+ rewritten.push({
337
+ ...commands[i],
338
+ cmd: `fill ${first.x} ${first.y} ${first.z} ${last.x} ${last.y} ${last.z} ${block}`,
339
+ })
340
+ stats.setblockMergedCommands = (stats.setblockMergedCommands ?? 0) + run.length
341
+ stats.setblockFillCommands = (stats.setblockFillCommands ?? 0) + 1
342
+ stats.setblockSavedCommands = (stats.setblockSavedCommands ?? 0) + (run.length - 1)
343
+ i = j
344
+ continue
345
+ }
346
+
347
+ rewritten.push(commands[i])
348
+ i++
349
+ }
350
+
351
+ return { commands: rewritten, stats }
352
+ }
353
+
354
+ function applySetblockBatchingInternal(functions: CommandFunction[]): Partial<OptimizationStats> {
355
+ const stats: Partial<OptimizationStats> = {
356
+ setblockMergedCommands: 0,
357
+ setblockFillCommands: 0,
358
+ setblockSavedCommands: 0,
359
+ }
360
+
361
+ for (const fn of functions) {
362
+ const batched = batchSetblocksInCommands(fn.commands)
363
+ fn.commands = batched.commands
364
+ mergeOptimizationStats(stats as OptimizationStats, batched.stats)
365
+ }
366
+
367
+ return stats
368
+ }
369
+
370
+ export function applyLICM(functions: CommandFunction[]): { functions: CommandFunction[]; stats: OptimizationStats } {
371
+ const optimized = cloneFunctions(functions)
372
+ const stats = createEmptyOptimizationStats()
373
+ stats.totalCommandsBefore = optimized.reduce((sum, fn) => sum + fn.commands.length, 0)
374
+ mergeOptimizationStats(stats, applyLICMInternal(optimized))
375
+ stats.totalCommandsAfter = optimized.reduce((sum, fn) => sum + fn.commands.length, 0)
376
+ return { functions: optimized, stats }
377
+ }
378
+
379
+ export function applyCSE(functions: CommandFunction[]): { functions: CommandFunction[]; stats: OptimizationStats } {
380
+ const optimized = cloneFunctions(functions)
381
+ const stats = createEmptyOptimizationStats()
382
+ stats.totalCommandsBefore = optimized.reduce((sum, fn) => sum + fn.commands.length, 0)
383
+ mergeOptimizationStats(stats, applyCSEInternal(optimized))
384
+ stats.totalCommandsAfter = optimized.reduce((sum, fn) => sum + fn.commands.length, 0)
385
+ return { functions: optimized, stats }
386
+ }
387
+
388
+ export function batchSetblocks(functions: CommandFunction[]): { functions: CommandFunction[]; stats: OptimizationStats } {
389
+ const optimized = cloneFunctions(functions)
390
+ const stats = createEmptyOptimizationStats()
391
+ stats.totalCommandsBefore = optimized.reduce((sum, fn) => sum + fn.commands.length, 0)
392
+ mergeOptimizationStats(stats, applySetblockBatchingInternal(optimized))
393
+ stats.totalCommandsAfter = optimized.reduce((sum, fn) => sum + fn.commands.length, 0)
394
+ return { functions: optimized, stats }
395
+ }
396
+
397
+ export function optimizeCommandFunctions(functions: CommandFunction[]): { functions: CommandFunction[]; stats: OptimizationStats } {
398
+ const initial = cloneFunctions(functions)
399
+ const stats = createEmptyOptimizationStats()
400
+ stats.totalCommandsBefore = initial.reduce((sum, fn) => sum + fn.commands.length, 0)
401
+
402
+ const licm = applyLICM(initial)
403
+ mergeOptimizationStats(stats, licm.stats)
404
+
405
+ const cse = applyCSE(licm.functions)
406
+ mergeOptimizationStats(stats, cse.stats)
407
+
408
+ const batched = batchSetblocks(cse.functions)
409
+ mergeOptimizationStats(stats, batched.stats)
410
+ stats.totalCommandsAfter = batched.functions.reduce((sum, fn) => sum + fn.commands.length, 0)
411
+
412
+ return {
413
+ functions: batched.functions,
414
+ stats,
415
+ }
416
+ }
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Optimization passes over IR.
3
+ *
4
+ * Each pass: IRFunction → IRFunction (pure transformation)
5
+ *
6
+ * Pipeline order:
7
+ * 1. constantFolding — evaluate constant expressions at compile time
8
+ * 2. copyPropagation — eliminate redundant copies
9
+ * 3. deadCodeElimination — remove unused assignments
10
+ * 4. commandMerging — MC-specific: merge chained execute conditions
11
+ */
12
+
13
+ import type { IRBlock, IRFunction, IRInstr, Operand } from '../ir/types'
14
+ import { createEmptyOptimizationStats, mergeOptimizationStats, type OptimizationStats } from './commands'
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Helpers
18
+ // ---------------------------------------------------------------------------
19
+
20
+ function isConst(op: Operand): op is { kind: 'const'; value: number } {
21
+ return op.kind === 'const'
22
+ }
23
+
24
+ function evalBinop(lhs: number, bop: string, rhs: number): number | null {
25
+ switch (bop) {
26
+ case '+': return lhs + rhs
27
+ case '-': return lhs - rhs
28
+ case '*': return lhs * rhs
29
+ case '/': return rhs === 0 ? null : Math.trunc(lhs / rhs) // MC uses truncated int division
30
+ case '%': return rhs === 0 ? null : lhs % rhs
31
+ default: return null
32
+ }
33
+ }
34
+
35
+ function evalCmp(lhs: number, cop: string, rhs: number): number {
36
+ switch (cop) {
37
+ case '==': return lhs === rhs ? 1 : 0
38
+ case '!=': return lhs !== rhs ? 1 : 0
39
+ case '<': return lhs < rhs ? 1 : 0
40
+ case '<=': return lhs <= rhs ? 1 : 0
41
+ case '>': return lhs > rhs ? 1 : 0
42
+ case '>=': return lhs >= rhs ? 1 : 0
43
+ default: return 0
44
+ }
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Pass 1: Constant Folding
49
+ // Evaluates expressions with all-constant operands at compile time.
50
+ // ---------------------------------------------------------------------------
51
+
52
+ export function constantFolding(fn: IRFunction): IRFunction {
53
+ return constantFoldingWithStats(fn).fn
54
+ }
55
+
56
+ export function constantFoldingWithStats(fn: IRFunction): { fn: IRFunction; stats: Partial<OptimizationStats> } {
57
+ let folded = 0
58
+ const newBlocks = fn.blocks.map(block => {
59
+ const newInstrs: IRInstr[] = []
60
+ for (const instr of block.instrs) {
61
+ if (instr.op === 'binop' && isConst(instr.lhs) && isConst(instr.rhs)) {
62
+ const result = evalBinop(instr.lhs.value, instr.bop, instr.rhs.value)
63
+ if (result !== null) {
64
+ folded++
65
+ newInstrs.push({ op: 'assign', dst: instr.dst, src: { kind: 'const', value: result } })
66
+ continue
67
+ }
68
+ }
69
+ if (instr.op === 'cmp' && isConst(instr.lhs) && isConst(instr.rhs)) {
70
+ const result = evalCmp(instr.lhs.value, instr.cop, instr.rhs.value)
71
+ folded++
72
+ newInstrs.push({ op: 'assign', dst: instr.dst, src: { kind: 'const', value: result } })
73
+ continue
74
+ }
75
+ newInstrs.push(instr)
76
+ }
77
+ return { ...block, instrs: newInstrs }
78
+ })
79
+ return { fn: { ...fn, blocks: newBlocks }, stats: { constantFolds: folded } }
80
+ }
81
+
82
+ // ---------------------------------------------------------------------------
83
+ // Pass 2: Copy Propagation
84
+ // Replaces uses of variables that are just copies with their source.
85
+ // e.g. t0 = x; y = t0 + 1 → y = x + 1
86
+ // ---------------------------------------------------------------------------
87
+
88
+ export function copyPropagation(fn: IRFunction): IRFunction {
89
+ // Build copy map within each block (single-block analysis for simplicity)
90
+ const newBlocks = fn.blocks.map(block => {
91
+ const copies = new Map<string, Operand>() // var → its source if it's a copy
92
+
93
+ function resolve(op: Operand): Operand {
94
+ if (op.kind !== 'var') return op
95
+ return copies.get(op.name) ?? op
96
+ }
97
+
98
+ const newInstrs: IRInstr[] = []
99
+ for (const instr of block.instrs) {
100
+ switch (instr.op) {
101
+ case 'assign': {
102
+ const src = resolve(instr.src)
103
+ // Only propagate scalars (var or const), not storage
104
+ if (src.kind === 'var' || src.kind === 'const') {
105
+ copies.set(instr.dst, src)
106
+ } else {
107
+ copies.delete(instr.dst)
108
+ }
109
+ newInstrs.push({ ...instr, src })
110
+ break
111
+ }
112
+ case 'binop':
113
+ copies.delete(instr.dst)
114
+ newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
115
+ break
116
+ case 'cmp':
117
+ copies.delete(instr.dst)
118
+ newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
119
+ break
120
+ case 'call':
121
+ if (instr.dst) copies.delete(instr.dst)
122
+ newInstrs.push({ ...instr, args: instr.args.map(resolve) })
123
+ break
124
+ default:
125
+ newInstrs.push(instr)
126
+ }
127
+ }
128
+ return { ...block, instrs: newInstrs }
129
+ })
130
+ return { ...fn, blocks: newBlocks }
131
+ }
132
+
133
+ // ---------------------------------------------------------------------------
134
+ // Pass 3: Dead Code Elimination
135
+ // Removes assignments to variables that are never read afterward.
136
+ // ---------------------------------------------------------------------------
137
+
138
+ export function deadCodeElimination(fn: IRFunction): IRFunction {
139
+ return deadCodeEliminationWithStats(fn).fn
140
+ }
141
+
142
+ export function deadCodeEliminationWithStats(fn: IRFunction): { fn: IRFunction; stats: Partial<OptimizationStats> } {
143
+ // Collect all reads across all blocks
144
+ const readVars = new Set<string>()
145
+
146
+ function markRead(op: Operand) {
147
+ if (op.kind === 'var') readVars.add(op.name)
148
+ }
149
+
150
+ function markRawReads(cmd: string) {
151
+ for (const match of cmd.matchAll(/\$[A-Za-z0-9_]+/g)) {
152
+ readVars.add(match[0])
153
+ }
154
+ }
155
+
156
+ for (const block of fn.blocks) {
157
+ for (const instr of block.instrs) {
158
+ if (instr.op === 'binop') { markRead(instr.lhs); markRead(instr.rhs) }
159
+ if (instr.op === 'cmp') { markRead(instr.lhs); markRead(instr.rhs) }
160
+ if (instr.op === 'call') { instr.args.forEach(markRead) }
161
+ if (instr.op === 'assign') { markRead(instr.src) }
162
+ if (instr.op === 'raw') { markRawReads(instr.cmd) }
163
+ }
164
+ // Terminator reads
165
+ const t = block.term
166
+ if (t.op === 'jump_if' || t.op === 'jump_unless') readVars.add(t.cond)
167
+ if (t.op === 'return' && t.value) markRead(t.value)
168
+ if (t.op === 'tick_yield') { /* no reads */ }
169
+ }
170
+
171
+ // Also keep params and globals
172
+ fn.params.forEach(p => readVars.add(p))
173
+
174
+ let removed = 0
175
+ const newBlocks = fn.blocks.map(block => ({
176
+ ...block,
177
+ instrs: block.instrs.filter(instr => {
178
+ // Only assignments/binops/cmps with an unused dst are candidates for removal
179
+ if (instr.op === 'assign' || instr.op === 'binop' || instr.op === 'cmp') {
180
+ const keep = readVars.has(instr.dst)
181
+ if (!keep) removed++
182
+ return keep
183
+ }
184
+ // calls may have side effects — keep them always
185
+ return true
186
+ }),
187
+ }))
188
+
189
+ return { fn: { ...fn, blocks: newBlocks }, stats: { deadCodeRemoved: removed } }
190
+ }
191
+
192
+ // ---------------------------------------------------------------------------
193
+ // Pipeline
194
+ // ---------------------------------------------------------------------------
195
+
196
+ export interface OptimizationPass {
197
+ name: string
198
+ run: (fn: IRFunction) => IRFunction
199
+ }
200
+
201
+ export const defaultPipeline: OptimizationPass[] = [
202
+ { name: 'constant-folding', run: constantFolding },
203
+ { name: 'copy-propagation', run: copyPropagation },
204
+ { name: 'dead-code-elimination', run: deadCodeElimination },
205
+ // commandMerging is applied during codegen (MC-specific)
206
+ ]
207
+
208
+ export function optimize(fn: IRFunction, passes = defaultPipeline): IRFunction {
209
+ return optimizeWithStats(fn, passes).fn
210
+ }
211
+
212
+ export function optimizeWithStats(fn: IRFunction, passes = defaultPipeline): { fn: IRFunction; stats: OptimizationStats } {
213
+ let current = fn
214
+ const stats = createEmptyOptimizationStats()
215
+
216
+ for (const pass of passes) {
217
+ if (pass.name === 'constant-folding') {
218
+ const result = constantFoldingWithStats(current)
219
+ current = result.fn
220
+ mergeOptimizationStats(stats, result.stats)
221
+ continue
222
+ }
223
+ if (pass.name === 'dead-code-elimination') {
224
+ const result = deadCodeEliminationWithStats(current)
225
+ current = result.fn
226
+ mergeOptimizationStats(stats, result.stats)
227
+ continue
228
+ }
229
+ current = pass.run(current)
230
+ }
231
+
232
+ return { fn: current, stats }
233
+ }