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,441 @@
1
+ import type { IRBlock, IRCommand, IRFunction, IRInstr, Operand, Terminator } from '../ir/types'
2
+ import { createEmptyOptimizationStats, mergeOptimizationStats, optimizeCommandFunctions, type OptimizationStats } from './commands'
3
+
4
+ const OBJ = 'rs'
5
+ const INLINE_THRESHOLD = 8
6
+
7
+ const BOP_OP: Record<string, string> = {
8
+ '+': '+=',
9
+ '-': '-=',
10
+ '*': '*=',
11
+ '/': '/=',
12
+ '%': '%=',
13
+ }
14
+
15
+ interface InlineBlock {
16
+ commands: IRCommand[]
17
+ continuation?: string
18
+ }
19
+
20
+ function varRef(name: string): string {
21
+ return name.startsWith('$') ? name : `$${name}`
22
+ }
23
+
24
+ function operandToScore(op: Operand): string {
25
+ if (op.kind === 'var') return `${varRef(op.name)} ${OBJ}`
26
+ if (op.kind === 'const') return `$const_${op.value} ${OBJ}`
27
+ throw new Error(`Cannot convert storage operand to score: ${op.path}`)
28
+ }
29
+
30
+ function emitInstr(instr: IRInstr, namespace: string): IRCommand[] {
31
+ const commands: IRCommand[] = []
32
+
33
+ switch (instr.op) {
34
+ case 'assign':
35
+ if (instr.src.kind === 'const') {
36
+ commands.push({ cmd: `scoreboard players set ${varRef(instr.dst)} ${OBJ} ${instr.src.value}` })
37
+ } else if (instr.src.kind === 'var') {
38
+ commands.push({
39
+ cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = ${varRef(instr.src.name)} ${OBJ}`,
40
+ })
41
+ } else {
42
+ commands.push({
43
+ cmd: `execute store result score ${varRef(instr.dst)} ${OBJ} run data get storage ${instr.src.path}`,
44
+ })
45
+ }
46
+ break
47
+
48
+ case 'binop':
49
+ commands.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, namespace))
50
+ commands.push({
51
+ cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} ${BOP_OP[instr.bop]} ${operandToScore(instr.rhs)}`,
52
+ })
53
+ break
54
+
55
+ case 'cmp': {
56
+ const dst = varRef(instr.dst)
57
+ const lhs = operandToScore(instr.lhs)
58
+ const rhs = operandToScore(instr.rhs)
59
+ commands.push({ cmd: `scoreboard players set ${dst} ${OBJ} 0` })
60
+ const op =
61
+ instr.cop === '==' ? 'if score' :
62
+ instr.cop === '!=' ? 'unless score' :
63
+ instr.cop === '<' ? 'if score' :
64
+ instr.cop === '<=' ? 'if score' :
65
+ instr.cop === '>' ? 'if score' :
66
+ 'if score'
67
+ const cmp =
68
+ instr.cop === '==' || instr.cop === '!=' ? '=' :
69
+ instr.cop
70
+ commands.push({
71
+ cmd: `execute ${op} ${lhs} ${cmp} ${rhs} run scoreboard players set ${dst} ${OBJ} 1`,
72
+ })
73
+ break
74
+ }
75
+
76
+ case 'call':
77
+ for (let i = 0; i < instr.args.length; i++) {
78
+ commands.push(...emitInstr({ op: 'assign', dst: `$p${i}`, src: instr.args[i] }, namespace))
79
+ }
80
+ commands.push({ cmd: `function ${namespace}:${instr.fn}` })
81
+ if (instr.dst) {
82
+ commands.push({
83
+ cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $ret ${OBJ}`,
84
+ })
85
+ }
86
+ break
87
+
88
+ case 'raw':
89
+ commands.push({ cmd: instr.cmd })
90
+ break
91
+ }
92
+
93
+ return commands
94
+ }
95
+
96
+ function emitReturn(term: Extract<Terminator, { op: 'return' }>): IRCommand[] {
97
+ const commands: IRCommand[] = []
98
+ if (term.value) {
99
+ commands.push(...emitInstr({ op: 'assign', dst: '$ret', src: term.value }, ''))
100
+ }
101
+ if (term.value?.kind === 'const') {
102
+ commands.push({ cmd: `return ${term.value.value}` })
103
+ } else if (term.value?.kind === 'var') {
104
+ commands.push({ cmd: `return run scoreboard players get ${varRef(term.value.name)} ${OBJ}` })
105
+ }
106
+ return commands
107
+ }
108
+
109
+ function markConditional(commands: IRCommand[]): IRCommand[] {
110
+ return commands.map(command => ({
111
+ ...command,
112
+ conditional: true,
113
+ }))
114
+ }
115
+
116
+ function cloneVisited(visited: Set<string>): Set<string> {
117
+ return new Set(visited)
118
+ }
119
+
120
+ function isRecursiveCommand(command: string, currentFn: string, namespace: string): boolean {
121
+ return command.includes(`function ${namespace}:${currentFn}`)
122
+ }
123
+
124
+ function getInlineableBlock(
125
+ block: IRBlock | undefined,
126
+ currentFn: string,
127
+ namespace: string
128
+ ): InlineBlock | null {
129
+ if (!block) return null
130
+ if (block.term.op === 'jump_if' || block.term.op === 'jump_unless' || block.term.op === 'tick_yield') {
131
+ return null
132
+ }
133
+
134
+ const commands = block.instrs.flatMap(instr => emitInstr(instr, namespace))
135
+ if (commands.some(command => isRecursiveCommand(command.cmd, currentFn, namespace))) {
136
+ return null
137
+ }
138
+
139
+ if (block.term.op === 'return') {
140
+ commands.push(...emitReturn(block.term))
141
+ }
142
+
143
+ if (commands.length > INLINE_THRESHOLD) {
144
+ return null
145
+ }
146
+
147
+ return {
148
+ commands,
149
+ continuation: block.term.op === 'jump' ? block.term.target : undefined,
150
+ }
151
+ }
152
+
153
+ function flattenBlock(
154
+ fn: IRFunction,
155
+ label: string,
156
+ namespace: string,
157
+ visited: Set<string>
158
+ ): IRCommand[] {
159
+ const blockMap = new Map(fn.blocks.map(block => [block.label, block]))
160
+ const block = blockMap.get(label)
161
+ if (!block) {
162
+ return []
163
+ }
164
+
165
+ if (visited.has(label)) {
166
+ return [{ cmd: `function ${namespace}:${fn.name}/${label}`, label }]
167
+ }
168
+
169
+ visited.add(label)
170
+
171
+ const commands: IRCommand[] = []
172
+ if (label === fn.blocks[0]?.label) {
173
+ for (let i = 0; i < fn.params.length; i++) {
174
+ commands.push({
175
+ cmd: `scoreboard players operation ${varRef(fn.params[i])} ${OBJ} = $p${i} ${OBJ}`,
176
+ })
177
+ }
178
+ }
179
+ commands.push(...block.instrs.flatMap(instr => emitInstr(instr, namespace)))
180
+ const term = block.term
181
+
182
+ switch (term.op) {
183
+ case 'jump':
184
+ commands.push(...flattenBlock(fn, term.target, namespace, visited))
185
+ return commands
186
+
187
+ case 'jump_if':
188
+ case 'jump_unless': {
189
+ const trueLabel = term.op === 'jump_if' ? term.then : term.else_
190
+ const falseLabel = term.op === 'jump_if' ? term.else_ : term.then
191
+ const trueRange = term.op === 'jump_if' ? '1..' : '..0'
192
+ const falseRange = term.op === 'jump_if' ? '..0' : '1..'
193
+ const trueBlock = getInlineableBlock(blockMap.get(trueLabel), fn.name, namespace)
194
+ const falseBlock = getInlineableBlock(blockMap.get(falseLabel), fn.name, namespace)
195
+
196
+ if (trueBlock && falseBlock) {
197
+ if (trueBlock.commands.length > 0) {
198
+ commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${trueRange}`, label: trueLabel })
199
+ commands.push(...markConditional(trueBlock.commands))
200
+ }
201
+ if (falseBlock.commands.length > 0) {
202
+ commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${falseRange}`, label: falseLabel })
203
+ commands.push(...markConditional(falseBlock.commands))
204
+ }
205
+
206
+ const continuation = trueBlock.continuation && trueBlock.continuation === falseBlock.continuation
207
+ ? trueBlock.continuation
208
+ : undefined
209
+ if (continuation) {
210
+ commands.push(...flattenBlock(fn, continuation, namespace, cloneVisited(visited)))
211
+ }
212
+ return commands
213
+ }
214
+
215
+ commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${trueRange} run function ${namespace}:${fn.name}/${trueLabel}` })
216
+ commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${falseRange} run function ${namespace}:${fn.name}/${falseLabel}` })
217
+ return commands
218
+ }
219
+
220
+ case 'return':
221
+ commands.push(...emitReturn(term))
222
+ return commands
223
+
224
+ case 'tick_yield':
225
+ commands.push({ cmd: `schedule function ${namespace}:${fn.name}/${term.continuation} 1t replace` })
226
+ return commands
227
+ }
228
+ }
229
+
230
+ function findVars(command: string): string[] {
231
+ return Array.from(command.matchAll(/\$[A-Za-z0-9_]+/g), match => match[0])
232
+ }
233
+
234
+ function parsePureWrite(command: string): { dst: string; reads: string[] } | null {
235
+ let match = command.match(/^scoreboard players set (\$[A-Za-z0-9_]+) rs -?\d+$/)
236
+ if (match) {
237
+ return { dst: match[1], reads: [] }
238
+ }
239
+
240
+ match = command.match(/^scoreboard players operation (\$[A-Za-z0-9_]+) rs = (\$[A-Za-z0-9_]+) rs$/)
241
+ if (match) {
242
+ return { dst: match[1], reads: [match[2]] }
243
+ }
244
+
245
+ match = command.match(/^execute .* run scoreboard players set (\$[A-Za-z0-9_]+) rs -?\d+$/)
246
+ if (match) {
247
+ return {
248
+ dst: match[1],
249
+ reads: findVars(command).filter(name => name !== match![1]),
250
+ }
251
+ }
252
+
253
+ return null
254
+ }
255
+
256
+ function deadStoreEliminate(commands: IRCommand[]): IRCommand[] {
257
+ const live = new Set<string>()
258
+ const kept: IRCommand[] = []
259
+
260
+ for (let i = commands.length - 1; i >= 0; i--) {
261
+ const command = commands[i]
262
+ const pureWrite = parsePureWrite(command.cmd)
263
+
264
+ if (pureWrite) {
265
+ pureWrite.reads.forEach(name => live.add(name))
266
+ if (!live.has(pureWrite.dst)) {
267
+ continue
268
+ }
269
+ live.delete(pureWrite.dst)
270
+ kept.push(command)
271
+ continue
272
+ }
273
+
274
+ findVars(command.cmd).forEach(name => live.add(name))
275
+ kept.push(command)
276
+ }
277
+
278
+ return kept.reverse()
279
+ }
280
+
281
+ function isInlineableFunction(
282
+ fn: IRFunction | undefined,
283
+ currentFn: string,
284
+ namespace: string
285
+ ): fn is IRFunction & { commands: IRCommand[] } {
286
+ if (!fn?.commands || fn.name === currentFn || fn.commands.length > INLINE_THRESHOLD) {
287
+ return false
288
+ }
289
+
290
+ return !fn.commands.some(command =>
291
+ isRecursiveCommand(command.cmd, currentFn, namespace) ||
292
+ isRecursiveCommand(command.cmd, fn.name, namespace)
293
+ )
294
+ }
295
+
296
+ function inlineConditionalCalls(
297
+ commands: IRCommand[],
298
+ functions: Map<string, IRFunction>,
299
+ currentFn: string,
300
+ namespace: string
301
+ ): IRCommand[] {
302
+ const optimized: IRCommand[] = []
303
+
304
+ for (const command of commands) {
305
+ const match = command.cmd.match(/^(execute .+) run function ([^:]+):(.+)$/)
306
+ if (!match || match[2] !== namespace) {
307
+ optimized.push(command)
308
+ continue
309
+ }
310
+
311
+ const target = functions.get(match[3])
312
+ if (!isInlineableFunction(target, currentFn, namespace)) {
313
+ optimized.push(command)
314
+ continue
315
+ }
316
+
317
+ optimized.push({ cmd: match[1], label: command.label })
318
+ optimized.push(...markConditional(target.commands))
319
+ }
320
+
321
+ return optimized
322
+ }
323
+
324
+ function invertExecuteCondition(command: string): string | null {
325
+ if (command.startsWith('execute if ')) {
326
+ return command.replace(/^execute if /, 'execute unless ')
327
+ }
328
+ if (command.startsWith('execute unless ')) {
329
+ return command.replace(/^execute unless /, 'execute if ')
330
+ }
331
+ return null
332
+ }
333
+
334
+ function eliminateBranchVariables(
335
+ commands: IRCommand[],
336
+ functions: Map<string, IRFunction>,
337
+ currentFn: string,
338
+ namespace: string
339
+ ): IRCommand[] {
340
+ const optimized: IRCommand[] = []
341
+
342
+ for (let i = 0; i < commands.length; i++) {
343
+ const init = commands[i]
344
+ const set = commands[i + 1]
345
+ const thenCmd = commands[i + 2]
346
+ const elseCmd = commands[i + 3]
347
+
348
+ const initMatch = init?.cmd.match(/^scoreboard players set (\$[A-Za-z0-9_]+) rs 0$/)
349
+ const setMatch = set?.cmd.match(/^((?:execute if|execute unless) .+) run scoreboard players set (\$[A-Za-z0-9_]+) rs 1$/)
350
+ const thenMatch = thenCmd?.cmd.match(/^execute if score (\$[A-Za-z0-9_]+) rs matches 1\.\. run function [^:]+:(.+)$/)
351
+ const elseMatch =
352
+ elseCmd?.cmd.match(/^execute if score (\$[A-Za-z0-9_]+) rs matches ..0 run function [^:]+:(.+)$/) ??
353
+ elseCmd?.cmd.match(/^execute unless score (\$[A-Za-z0-9_]+) rs matches 1\.\. run function [^:]+:(.+)$/)
354
+
355
+ if (!initMatch || !setMatch || !thenMatch || !elseMatch) {
356
+ optimized.push(init)
357
+ continue
358
+ }
359
+
360
+ const branchVar = initMatch[1]
361
+ if (setMatch[2] !== branchVar || thenMatch[1] !== branchVar || elseMatch[1] !== branchVar) {
362
+ optimized.push(init)
363
+ continue
364
+ }
365
+
366
+ const thenFn = functions.get(thenMatch[2])
367
+ const elseFn = functions.get(elseMatch[2])
368
+ if (!isInlineableFunction(thenFn, currentFn, namespace) || !isInlineableFunction(elseFn, currentFn, namespace)) {
369
+ optimized.push(init)
370
+ continue
371
+ }
372
+
373
+ const thenCondition = setMatch[1]
374
+ const elseCondition = invertExecuteCondition(thenCondition)
375
+ if (!elseCondition) {
376
+ optimized.push(init)
377
+ continue
378
+ }
379
+
380
+ optimized.push({ cmd: thenCondition })
381
+ optimized.push(...markConditional(thenFn.commands))
382
+ if (elseFn.commands.length > 0) {
383
+ optimized.push({ cmd: elseCondition })
384
+ optimized.push(...markConditional(elseFn.commands))
385
+ }
386
+ i += 3
387
+ }
388
+
389
+ return optimized
390
+ }
391
+
392
+ export function optimizeFunctionForStructure(
393
+ fn: IRFunction,
394
+ functions: Map<string, IRFunction>,
395
+ namespace: string
396
+ ): IRCommand[] {
397
+ if (fn.blocks.length === 0) {
398
+ return []
399
+ }
400
+
401
+ const linear = flattenBlock(fn, fn.blocks[0].label, namespace, new Set<string>())
402
+ const branchEliminated = eliminateBranchVariables(linear, functions, fn.name, namespace)
403
+ const inlined = inlineConditionalCalls(branchEliminated, functions, fn.name, namespace)
404
+ return deadStoreEliminate(inlined)
405
+ }
406
+
407
+ export function optimizeForStructure(functions: IRFunction[], namespace = 'redscript'): IRFunction[] {
408
+ return optimizeForStructureWithStats(functions, namespace).functions
409
+ }
410
+
411
+ export function optimizeForStructureWithStats(
412
+ functions: IRFunction[],
413
+ namespace = 'redscript'
414
+ ): { functions: IRFunction[]; stats: OptimizationStats } {
415
+ const staged = new Map(functions.map(fn => [fn.name, { ...fn }]))
416
+
417
+ for (const fn of staged.values()) {
418
+ fn.commands = flattenBlock(fn, fn.blocks[0]?.label ?? 'entry', namespace, new Set<string>())
419
+ }
420
+
421
+ for (const fn of staged.values()) {
422
+ fn.commands = optimizeFunctionForStructure(fn, staged, namespace)
423
+ }
424
+
425
+ const optimizedCommands = optimizeCommandFunctions(
426
+ Array.from(staged.values()).map(fn => ({
427
+ name: fn.name,
428
+ commands: fn.commands ?? [],
429
+ }))
430
+ )
431
+ const stats = createEmptyOptimizationStats()
432
+ mergeOptimizationStats(stats, optimizedCommands.stats)
433
+
434
+ return {
435
+ functions: Array.from(staged.values()).map(fn => ({
436
+ ...fn,
437
+ commands: optimizedCommands.functions.find(candidate => candidate.name === fn.name)?.commands ?? fn.commands,
438
+ })),
439
+ stats,
440
+ }
441
+ }