redscript-mc 1.2.30 → 2.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 (269) hide show
  1. package/.claude/commands/build-test.md +10 -0
  2. package/.claude/commands/deploy-demo.md +12 -0
  3. package/.claude/commands/stage-status.md +13 -0
  4. package/.claude/settings.json +12 -0
  5. package/.github/workflows/ci.yml +1 -0
  6. package/CLAUDE.md +231 -0
  7. package/demo.gif +0 -0
  8. package/dist/cli.js +2 -554
  9. package/dist/compile.js +2 -266
  10. package/dist/index.js +2 -159
  11. package/dist/lowering/index.js +5 -3
  12. package/dist/src/__tests__/cli.test.d.ts +1 -0
  13. package/dist/src/__tests__/cli.test.js +104 -0
  14. package/dist/src/__tests__/codegen.test.d.ts +1 -0
  15. package/dist/src/__tests__/codegen.test.js +152 -0
  16. package/dist/src/__tests__/compile-all.test.d.ts +10 -0
  17. package/dist/src/__tests__/compile-all.test.js +108 -0
  18. package/dist/src/__tests__/dce.test.d.ts +1 -0
  19. package/dist/src/__tests__/dce.test.js +102 -0
  20. package/dist/src/__tests__/diagnostics.test.d.ts +4 -0
  21. package/dist/src/__tests__/diagnostics.test.js +177 -0
  22. package/dist/src/__tests__/e2e.test.d.ts +6 -0
  23. package/dist/src/__tests__/e2e.test.js +1789 -0
  24. package/dist/src/__tests__/entity-types.test.d.ts +1 -0
  25. package/dist/src/__tests__/entity-types.test.js +203 -0
  26. package/dist/src/__tests__/formatter.test.d.ts +1 -0
  27. package/dist/src/__tests__/formatter.test.js +40 -0
  28. package/dist/src/__tests__/lexer.test.d.ts +1 -0
  29. package/dist/src/__tests__/lexer.test.js +343 -0
  30. package/dist/src/__tests__/lowering.test.d.ts +1 -0
  31. package/dist/src/__tests__/lowering.test.js +1015 -0
  32. package/dist/src/__tests__/macro.test.d.ts +8 -0
  33. package/dist/src/__tests__/macro.test.js +306 -0
  34. package/dist/src/__tests__/mc-integration.test.d.ts +12 -0
  35. package/dist/src/__tests__/mc-integration.test.js +817 -0
  36. package/dist/src/__tests__/mc-syntax.test.d.ts +1 -0
  37. package/dist/src/__tests__/mc-syntax.test.js +124 -0
  38. package/dist/src/__tests__/nbt.test.d.ts +1 -0
  39. package/dist/src/__tests__/nbt.test.js +82 -0
  40. package/dist/src/__tests__/optimizer-advanced.test.d.ts +1 -0
  41. package/dist/src/__tests__/optimizer-advanced.test.js +124 -0
  42. package/dist/src/__tests__/optimizer.test.d.ts +1 -0
  43. package/dist/src/__tests__/optimizer.test.js +149 -0
  44. package/dist/src/__tests__/parser.test.d.ts +1 -0
  45. package/dist/src/__tests__/parser.test.js +807 -0
  46. package/dist/src/__tests__/repl.test.d.ts +1 -0
  47. package/dist/src/__tests__/repl.test.js +27 -0
  48. package/dist/src/__tests__/runtime.test.d.ts +1 -0
  49. package/dist/src/__tests__/runtime.test.js +289 -0
  50. package/dist/src/__tests__/stdlib-advanced.test.d.ts +4 -0
  51. package/dist/src/__tests__/stdlib-advanced.test.js +374 -0
  52. package/dist/src/__tests__/stdlib-bigint.test.d.ts +7 -0
  53. package/dist/src/__tests__/stdlib-bigint.test.js +426 -0
  54. package/dist/src/__tests__/stdlib-math.test.d.ts +7 -0
  55. package/dist/src/__tests__/stdlib-math.test.js +351 -0
  56. package/dist/src/__tests__/stdlib-vec.test.d.ts +4 -0
  57. package/dist/src/__tests__/stdlib-vec.test.js +263 -0
  58. package/dist/src/__tests__/structure-optimizer.test.d.ts +1 -0
  59. package/dist/src/__tests__/structure-optimizer.test.js +33 -0
  60. package/dist/src/__tests__/typechecker.test.d.ts +1 -0
  61. package/dist/src/__tests__/typechecker.test.js +552 -0
  62. package/dist/src/__tests__/var-allocator.test.d.ts +1 -0
  63. package/dist/src/__tests__/var-allocator.test.js +69 -0
  64. package/dist/src/ast/types.d.ts +515 -0
  65. package/dist/src/ast/types.js +9 -0
  66. package/dist/src/builtins/metadata.d.ts +36 -0
  67. package/dist/src/builtins/metadata.js +1014 -0
  68. package/dist/src/cli.d.ts +11 -0
  69. package/dist/src/cli.js +443 -0
  70. package/dist/src/codegen/cmdblock/index.d.ts +26 -0
  71. package/dist/src/codegen/cmdblock/index.js +45 -0
  72. package/dist/src/codegen/mcfunction/index.d.ts +40 -0
  73. package/dist/src/codegen/mcfunction/index.js +606 -0
  74. package/dist/src/codegen/structure/index.d.ts +24 -0
  75. package/dist/src/codegen/structure/index.js +279 -0
  76. package/dist/src/codegen/var-allocator.d.ts +45 -0
  77. package/dist/src/codegen/var-allocator.js +104 -0
  78. package/dist/src/compile.d.ts +37 -0
  79. package/dist/src/compile.js +165 -0
  80. package/dist/src/diagnostics/index.d.ts +44 -0
  81. package/dist/src/diagnostics/index.js +140 -0
  82. package/dist/src/events/types.d.ts +35 -0
  83. package/dist/src/events/types.js +59 -0
  84. package/dist/src/formatter/index.d.ts +1 -0
  85. package/dist/src/formatter/index.js +26 -0
  86. package/dist/src/index.d.ts +22 -0
  87. package/dist/src/index.js +45 -0
  88. package/dist/src/ir/builder.d.ts +33 -0
  89. package/dist/src/ir/builder.js +99 -0
  90. package/dist/src/ir/types.d.ts +132 -0
  91. package/dist/src/ir/types.js +15 -0
  92. package/dist/src/lexer/index.d.ts +37 -0
  93. package/dist/src/lexer/index.js +569 -0
  94. package/dist/src/lowering/index.d.ts +188 -0
  95. package/dist/src/lowering/index.js +3405 -0
  96. package/dist/src/mc-test/client.d.ts +128 -0
  97. package/dist/src/mc-test/client.js +174 -0
  98. package/dist/src/mc-test/runner.d.ts +28 -0
  99. package/dist/src/mc-test/runner.js +151 -0
  100. package/dist/src/mc-test/setup.d.ts +11 -0
  101. package/dist/src/mc-test/setup.js +98 -0
  102. package/dist/src/mc-validator/index.d.ts +17 -0
  103. package/dist/src/mc-validator/index.js +322 -0
  104. package/dist/src/nbt/index.d.ts +86 -0
  105. package/dist/src/nbt/index.js +250 -0
  106. package/dist/src/optimizer/commands.d.ts +38 -0
  107. package/dist/src/optimizer/commands.js +451 -0
  108. package/dist/src/optimizer/dce.d.ts +34 -0
  109. package/dist/src/optimizer/dce.js +639 -0
  110. package/dist/src/optimizer/passes.d.ts +34 -0
  111. package/dist/src/optimizer/passes.js +243 -0
  112. package/dist/src/optimizer/structure.d.ts +9 -0
  113. package/dist/src/optimizer/structure.js +356 -0
  114. package/dist/src/parser/index.d.ts +93 -0
  115. package/dist/src/parser/index.js +1687 -0
  116. package/dist/src/repl.d.ts +16 -0
  117. package/dist/src/repl.js +165 -0
  118. package/dist/src/runtime/index.d.ts +107 -0
  119. package/dist/src/runtime/index.js +1409 -0
  120. package/dist/src/typechecker/index.d.ts +61 -0
  121. package/dist/src/typechecker/index.js +1034 -0
  122. package/dist/src/types/entity-hierarchy.d.ts +29 -0
  123. package/dist/src/types/entity-hierarchy.js +107 -0
  124. package/dist/src2/__tests__/e2e/basic.test.d.ts +8 -0
  125. package/dist/src2/__tests__/e2e/basic.test.js +140 -0
  126. package/dist/src2/__tests__/e2e/macros.test.d.ts +9 -0
  127. package/dist/src2/__tests__/e2e/macros.test.js +182 -0
  128. package/dist/src2/__tests__/e2e/migrate.test.d.ts +13 -0
  129. package/dist/src2/__tests__/e2e/migrate.test.js +2739 -0
  130. package/dist/src2/__tests__/hir/desugar.test.d.ts +1 -0
  131. package/dist/src2/__tests__/hir/desugar.test.js +234 -0
  132. package/dist/src2/__tests__/lir/lower.test.d.ts +1 -0
  133. package/dist/src2/__tests__/lir/lower.test.js +559 -0
  134. package/dist/src2/__tests__/lir/types.test.d.ts +1 -0
  135. package/dist/src2/__tests__/lir/types.test.js +185 -0
  136. package/dist/src2/__tests__/lir/verify.test.d.ts +1 -0
  137. package/dist/src2/__tests__/lir/verify.test.js +221 -0
  138. package/dist/src2/__tests__/mir/arithmetic.test.d.ts +1 -0
  139. package/dist/src2/__tests__/mir/arithmetic.test.js +130 -0
  140. package/dist/src2/__tests__/mir/control-flow.test.d.ts +1 -0
  141. package/dist/src2/__tests__/mir/control-flow.test.js +205 -0
  142. package/dist/src2/__tests__/mir/verify.test.d.ts +1 -0
  143. package/dist/src2/__tests__/mir/verify.test.js +223 -0
  144. package/dist/src2/__tests__/optimizer/block_merge.test.d.ts +1 -0
  145. package/dist/src2/__tests__/optimizer/block_merge.test.js +78 -0
  146. package/dist/src2/__tests__/optimizer/branch_simplify.test.d.ts +1 -0
  147. package/dist/src2/__tests__/optimizer/branch_simplify.test.js +58 -0
  148. package/dist/src2/__tests__/optimizer/constant_fold.test.d.ts +1 -0
  149. package/dist/src2/__tests__/optimizer/constant_fold.test.js +131 -0
  150. package/dist/src2/__tests__/optimizer/copy_prop.test.d.ts +1 -0
  151. package/dist/src2/__tests__/optimizer/copy_prop.test.js +91 -0
  152. package/dist/src2/__tests__/optimizer/dce.test.d.ts +1 -0
  153. package/dist/src2/__tests__/optimizer/dce.test.js +76 -0
  154. package/dist/src2/__tests__/optimizer/pipeline.test.d.ts +1 -0
  155. package/dist/src2/__tests__/optimizer/pipeline.test.js +102 -0
  156. package/dist/src2/emit/compile.d.ts +19 -0
  157. package/dist/src2/emit/compile.js +80 -0
  158. package/dist/src2/emit/index.d.ts +17 -0
  159. package/dist/src2/emit/index.js +172 -0
  160. package/dist/src2/hir/lower.d.ts +15 -0
  161. package/dist/src2/hir/lower.js +378 -0
  162. package/dist/src2/hir/types.d.ts +373 -0
  163. package/dist/src2/hir/types.js +16 -0
  164. package/dist/src2/lir/lower.d.ts +15 -0
  165. package/dist/src2/lir/lower.js +453 -0
  166. package/dist/src2/lir/types.d.ts +136 -0
  167. package/dist/src2/lir/types.js +11 -0
  168. package/dist/src2/lir/verify.d.ts +14 -0
  169. package/dist/src2/lir/verify.js +113 -0
  170. package/dist/src2/mir/lower.d.ts +9 -0
  171. package/dist/src2/mir/lower.js +1030 -0
  172. package/dist/src2/mir/macro.d.ts +22 -0
  173. package/dist/src2/mir/macro.js +168 -0
  174. package/dist/src2/mir/types.d.ts +183 -0
  175. package/dist/src2/mir/types.js +11 -0
  176. package/dist/src2/mir/verify.d.ts +16 -0
  177. package/dist/src2/mir/verify.js +216 -0
  178. package/dist/src2/optimizer/block_merge.d.ts +12 -0
  179. package/dist/src2/optimizer/block_merge.js +84 -0
  180. package/dist/src2/optimizer/branch_simplify.d.ts +9 -0
  181. package/dist/src2/optimizer/branch_simplify.js +28 -0
  182. package/dist/src2/optimizer/constant_fold.d.ts +10 -0
  183. package/dist/src2/optimizer/constant_fold.js +85 -0
  184. package/dist/src2/optimizer/copy_prop.d.ts +9 -0
  185. package/dist/src2/optimizer/copy_prop.js +113 -0
  186. package/dist/src2/optimizer/dce.d.ts +8 -0
  187. package/dist/src2/optimizer/dce.js +155 -0
  188. package/dist/src2/optimizer/pipeline.d.ts +10 -0
  189. package/dist/src2/optimizer/pipeline.js +42 -0
  190. package/dist/tsconfig.tsbuildinfo +1 -0
  191. package/docs/compiler-pipeline-redesign.md +2243 -0
  192. package/docs/optimization-ideas.md +1076 -0
  193. package/editors/vscode/package-lock.json +3 -3
  194. package/editors/vscode/package.json +1 -1
  195. package/jest.config.js +1 -1
  196. package/package.json +6 -5
  197. package/scripts/postbuild.js +15 -0
  198. package/src/__tests__/cli.test.ts +8 -220
  199. package/src/__tests__/dce.test.ts +11 -56
  200. package/src/__tests__/diagnostics.test.ts +59 -38
  201. package/src/__tests__/mc-integration.test.ts +1 -2
  202. package/src/ast/types.ts +6 -1
  203. package/src/cli.ts +29 -156
  204. package/src/compile.ts +6 -162
  205. package/src/index.ts +14 -178
  206. package/src/mc-test/runner.ts +4 -3
  207. package/src/parser/index.ts +1 -1
  208. package/src/repl.ts +1 -1
  209. package/src/runtime/index.ts +1 -1
  210. package/src2/__tests__/e2e/basic.test.ts +154 -0
  211. package/src2/__tests__/e2e/macros.test.ts +199 -0
  212. package/src2/__tests__/e2e/migrate.test.ts +3008 -0
  213. package/src2/__tests__/hir/desugar.test.ts +263 -0
  214. package/src2/__tests__/lir/lower.test.ts +619 -0
  215. package/src2/__tests__/lir/types.test.ts +207 -0
  216. package/src2/__tests__/lir/verify.test.ts +249 -0
  217. package/src2/__tests__/mir/arithmetic.test.ts +156 -0
  218. package/src2/__tests__/mir/control-flow.test.ts +242 -0
  219. package/src2/__tests__/mir/verify.test.ts +254 -0
  220. package/src2/__tests__/optimizer/block_merge.test.ts +84 -0
  221. package/src2/__tests__/optimizer/branch_simplify.test.ts +64 -0
  222. package/src2/__tests__/optimizer/constant_fold.test.ts +145 -0
  223. package/src2/__tests__/optimizer/copy_prop.test.ts +99 -0
  224. package/src2/__tests__/optimizer/dce.test.ts +83 -0
  225. package/src2/__tests__/optimizer/pipeline.test.ts +116 -0
  226. package/src2/emit/compile.ts +99 -0
  227. package/src2/emit/index.ts +222 -0
  228. package/src2/hir/lower.ts +428 -0
  229. package/src2/hir/types.ts +216 -0
  230. package/src2/lir/lower.ts +556 -0
  231. package/src2/lir/types.ts +109 -0
  232. package/src2/lir/verify.ts +129 -0
  233. package/src2/mir/lower.ts +1160 -0
  234. package/src2/mir/macro.ts +167 -0
  235. package/src2/mir/types.ts +106 -0
  236. package/src2/mir/verify.ts +218 -0
  237. package/src2/optimizer/block_merge.ts +93 -0
  238. package/src2/optimizer/branch_simplify.ts +27 -0
  239. package/src2/optimizer/constant_fold.ts +88 -0
  240. package/src2/optimizer/copy_prop.ts +106 -0
  241. package/src2/optimizer/dce.ts +133 -0
  242. package/src2/optimizer/pipeline.ts +44 -0
  243. package/tsconfig.json +2 -2
  244. package/src/__tests__/codegen.test.ts +0 -161
  245. package/src/__tests__/e2e.test.ts +0 -2039
  246. package/src/__tests__/entity-types.test.ts +0 -236
  247. package/src/__tests__/lowering.test.ts +0 -1185
  248. package/src/__tests__/macro.test.ts +0 -343
  249. package/src/__tests__/nbt.test.ts +0 -58
  250. package/src/__tests__/optimizer-advanced.test.ts +0 -144
  251. package/src/__tests__/optimizer.test.ts +0 -162
  252. package/src/__tests__/runtime.test.ts +0 -305
  253. package/src/__tests__/stdlib-advanced.test.ts +0 -379
  254. package/src/__tests__/stdlib-bigint.test.ts +0 -427
  255. package/src/__tests__/stdlib-math.test.ts +0 -374
  256. package/src/__tests__/stdlib-vec.test.ts +0 -259
  257. package/src/__tests__/structure-optimizer.test.ts +0 -38
  258. package/src/__tests__/var-allocator.test.ts +0 -75
  259. package/src/codegen/cmdblock/index.ts +0 -63
  260. package/src/codegen/mcfunction/index.ts +0 -662
  261. package/src/codegen/structure/index.ts +0 -346
  262. package/src/codegen/var-allocator.ts +0 -104
  263. package/src/ir/builder.ts +0 -116
  264. package/src/ir/types.ts +0 -134
  265. package/src/lowering/index.ts +0 -3876
  266. package/src/optimizer/commands.ts +0 -534
  267. package/src/optimizer/dce.ts +0 -679
  268. package/src/optimizer/passes.ts +0 -250
  269. package/src/optimizer/structure.ts +0 -450
@@ -1,250 +0,0 @@
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
- /**
99
- * Invalidate all copies that became stale because `written` was modified.
100
- * When $y is overwritten, any mapping copies[$tmp] = $y is now stale:
101
- * reading $tmp would return the OLD $y value via the copy, but $y now holds
102
- * a different value. Remove both the direct entry (copies[$y]) and every
103
- * reverse entry that points at $y.
104
- */
105
- function invalidate(written: string): void {
106
- copies.delete(written)
107
- for (const [k, v] of copies) {
108
- if (v.kind === 'var' && v.name === written) copies.delete(k)
109
- }
110
- }
111
-
112
- const newInstrs: IRInstr[] = []
113
- for (const instr of block.instrs) {
114
- switch (instr.op) {
115
- case 'assign': {
116
- const src = resolve(instr.src)
117
- invalidate(instr.dst)
118
- // Only propagate scalars (var or const), not storage
119
- if (src.kind === 'var' || src.kind === 'const') {
120
- copies.set(instr.dst, src)
121
- }
122
- newInstrs.push({ ...instr, src })
123
- break
124
- }
125
- case 'binop':
126
- invalidate(instr.dst)
127
- newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
128
- break
129
- case 'cmp':
130
- invalidate(instr.dst)
131
- newInstrs.push({ ...instr, lhs: resolve(instr.lhs), rhs: resolve(instr.rhs) })
132
- break
133
- case 'call':
134
- if (instr.dst) invalidate(instr.dst)
135
- newInstrs.push({ ...instr, args: instr.args.map(resolve) })
136
- break
137
- default:
138
- newInstrs.push(instr)
139
- }
140
- }
141
- return { ...block, instrs: newInstrs }
142
- })
143
- return { ...fn, blocks: newBlocks }
144
- }
145
-
146
- // ---------------------------------------------------------------------------
147
- // Pass 3: Dead Code Elimination
148
- // Removes assignments to variables that are never read afterward.
149
- // ---------------------------------------------------------------------------
150
-
151
- export function deadCodeElimination(fn: IRFunction): IRFunction {
152
- return deadCodeEliminationWithStats(fn).fn
153
- }
154
-
155
- export function deadCodeEliminationWithStats(fn: IRFunction): { fn: IRFunction; stats: Partial<OptimizationStats> } {
156
- // Collect all reads across all blocks
157
- const readVars = new Set<string>()
158
-
159
- function markRead(op: Operand) {
160
- if (op.kind === 'var') readVars.add(op.name)
161
- }
162
-
163
- function markRawReads(cmd: string) {
164
- for (const match of cmd.matchAll(/\$[A-Za-z0-9_]+/g)) {
165
- readVars.add(match[0])
166
- }
167
- }
168
-
169
- for (const block of fn.blocks) {
170
- for (const instr of block.instrs) {
171
- if (instr.op === 'binop') { markRead(instr.lhs); markRead(instr.rhs) }
172
- if (instr.op === 'cmp') { markRead(instr.lhs); markRead(instr.rhs) }
173
- if (instr.op === 'call') { instr.args.forEach(markRead) }
174
- if (instr.op === 'assign') { markRead(instr.src) }
175
- if (instr.op === 'raw') { markRawReads(instr.cmd) }
176
- }
177
- // Terminator reads
178
- const t = block.term
179
- if (t.op === 'jump_if' || t.op === 'jump_unless') readVars.add(t.cond)
180
- if (t.op === 'return' && t.value) markRead(t.value)
181
- if (t.op === 'tick_yield') { /* no reads */ }
182
- }
183
-
184
- // Also keep params and globals
185
- fn.params.forEach(p => readVars.add(p))
186
-
187
- let removed = 0
188
- const newBlocks = fn.blocks.map(block => ({
189
- ...block,
190
- instrs: block.instrs.filter(instr => {
191
- // Only assignments/binops/cmps with an unused dst are candidates for removal
192
- if (instr.op === 'assign' || instr.op === 'binop' || instr.op === 'cmp') {
193
- // Always keep assignments to global variables (they may be read by other functions)
194
- // Temps are $t0, $t1, ...; params are $p0, $p1, ...; locals are $_0, $_1, ...
195
- // Everything else is a potential global
196
- const isTemp = /^\$t\d+$/.test(instr.dst) || /^\$p\d+$/.test(instr.dst) || /^\$_\d+$/.test(instr.dst)
197
- const keep = !isTemp || readVars.has(instr.dst)
198
- if (!keep) removed++
199
- return keep
200
- }
201
- // calls may have side effects — keep them always
202
- return true
203
- }),
204
- }))
205
-
206
- return { fn: { ...fn, blocks: newBlocks }, stats: { deadCodeRemoved: removed } }
207
- }
208
-
209
- // ---------------------------------------------------------------------------
210
- // Pipeline
211
- // ---------------------------------------------------------------------------
212
-
213
- export interface OptimizationPass {
214
- name: string
215
- run: (fn: IRFunction) => IRFunction
216
- }
217
-
218
- export const defaultPipeline: OptimizationPass[] = [
219
- { name: 'constant-folding', run: constantFolding },
220
- { name: 'copy-propagation', run: copyPropagation },
221
- { name: 'dead-code-elimination', run: deadCodeElimination },
222
- // commandMerging is applied during codegen (MC-specific)
223
- ]
224
-
225
- export function optimize(fn: IRFunction, passes = defaultPipeline): IRFunction {
226
- return optimizeWithStats(fn, passes).fn
227
- }
228
-
229
- export function optimizeWithStats(fn: IRFunction, passes = defaultPipeline): { fn: IRFunction; stats: OptimizationStats } {
230
- let current = fn
231
- const stats = createEmptyOptimizationStats()
232
-
233
- for (const pass of passes) {
234
- if (pass.name === 'constant-folding') {
235
- const result = constantFoldingWithStats(current)
236
- current = result.fn
237
- mergeOptimizationStats(stats, result.stats)
238
- continue
239
- }
240
- if (pass.name === 'dead-code-elimination') {
241
- const result = deadCodeEliminationWithStats(current)
242
- current = result.fn
243
- mergeOptimizationStats(stats, result.stats)
244
- continue
245
- }
246
- current = pass.run(current)
247
- }
248
-
249
- return { fn: current, stats }
250
- }
@@ -1,450 +0,0 @@
1
- import type { IRBlock, IRCommand, IRFunction, IRInstr, Operand, Terminator } from '../ir/types'
2
- import { createEmptyOptimizationStats, mergeOptimizationStats, optimizeCommandFunctions, setOptimizerObjective, type OptimizationStats } from './commands'
3
-
4
- let OBJ = 'rs'
5
- export function setStructureObjective(obj: string): void {
6
- OBJ = obj
7
- setOptimizerObjective(obj)
8
- }
9
- const INLINE_THRESHOLD = 8
10
-
11
- const BOP_OP: Record<string, string> = {
12
- '+': '+=',
13
- '-': '-=',
14
- '*': '*=',
15
- '/': '/=',
16
- '%': '%=',
17
- }
18
-
19
- interface InlineBlock {
20
- commands: IRCommand[]
21
- continuation?: string
22
- }
23
-
24
- function varRef(name: string): string {
25
- return name.startsWith('$') ? name : `$${name}`
26
- }
27
-
28
- function operandToScore(op: Operand): string {
29
- if (op.kind === 'var') return `${varRef(op.name)} ${OBJ}`
30
- if (op.kind === 'const') return `$const_${op.value} ${OBJ}`
31
- if (op.kind === 'param') return `$p${op.index} ${OBJ}`
32
- throw new Error(`Cannot convert storage operand to score: ${(op as any).path}`)
33
- }
34
-
35
- function emitInstr(instr: IRInstr, namespace: string): IRCommand[] {
36
- const commands: IRCommand[] = []
37
-
38
- switch (instr.op) {
39
- case 'assign':
40
- if (instr.src.kind === 'const') {
41
- commands.push({ cmd: `scoreboard players set ${varRef(instr.dst)} ${OBJ} ${instr.src.value}` })
42
- } else if (instr.src.kind === 'var') {
43
- commands.push({
44
- cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = ${varRef(instr.src.name)} ${OBJ}`,
45
- })
46
- } else if (instr.src.kind === 'param') {
47
- commands.push({
48
- cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $p${instr.src.index} ${OBJ}`,
49
- })
50
- } else {
51
- commands.push({
52
- cmd: `execute store result score ${varRef(instr.dst)} ${OBJ} run data get storage ${instr.src.path}`,
53
- })
54
- }
55
- break
56
-
57
- case 'binop':
58
- commands.push(...emitInstr({ op: 'assign', dst: instr.dst, src: instr.lhs }, namespace))
59
- commands.push({
60
- cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} ${BOP_OP[instr.bop]} ${operandToScore(instr.rhs)}`,
61
- })
62
- break
63
-
64
- case 'cmp': {
65
- const dst = varRef(instr.dst)
66
- const lhs = operandToScore(instr.lhs)
67
- const rhs = operandToScore(instr.rhs)
68
- commands.push({ cmd: `scoreboard players set ${dst} ${OBJ} 0` })
69
- const op =
70
- instr.cop === '==' ? 'if score' :
71
- instr.cop === '!=' ? 'unless score' :
72
- instr.cop === '<' ? 'if score' :
73
- instr.cop === '<=' ? 'if score' :
74
- instr.cop === '>' ? 'if score' :
75
- 'if score'
76
- const cmp =
77
- instr.cop === '==' || instr.cop === '!=' ? '=' :
78
- instr.cop
79
- commands.push({
80
- cmd: `execute ${op} ${lhs} ${cmp} ${rhs} run scoreboard players set ${dst} ${OBJ} 1`,
81
- })
82
- break
83
- }
84
-
85
- case 'call':
86
- for (let i = 0; i < instr.args.length; i++) {
87
- commands.push(...emitInstr({ op: 'assign', dst: `$p${i}`, src: instr.args[i] }, namespace))
88
- }
89
- commands.push({ cmd: `function ${namespace}:${instr.fn}` })
90
- if (instr.dst) {
91
- commands.push({
92
- cmd: `scoreboard players operation ${varRef(instr.dst)} ${OBJ} = $ret ${OBJ}`,
93
- })
94
- }
95
- break
96
-
97
- case 'raw':
98
- commands.push({ cmd: instr.cmd })
99
- break
100
- }
101
-
102
- return commands
103
- }
104
-
105
- function emitReturn(term: Extract<Terminator, { op: 'return' }>): IRCommand[] {
106
- const commands: IRCommand[] = []
107
- if (term.value) {
108
- commands.push(...emitInstr({ op: 'assign', dst: '$ret', src: term.value }, ''))
109
- }
110
- if (term.value?.kind === 'const') {
111
- commands.push({ cmd: `return ${term.value.value}` })
112
- } else if (term.value?.kind === 'var') {
113
- commands.push({ cmd: `return run scoreboard players get ${varRef(term.value.name)} ${OBJ}` })
114
- }
115
- return commands
116
- }
117
-
118
- function markConditional(commands: IRCommand[]): IRCommand[] {
119
- return commands.map(command => ({
120
- ...command,
121
- conditional: true,
122
- }))
123
- }
124
-
125
- function cloneVisited(visited: Set<string>): Set<string> {
126
- return new Set(visited)
127
- }
128
-
129
- function isRecursiveCommand(command: string, currentFn: string, namespace: string): boolean {
130
- return command.includes(`function ${namespace}:${currentFn}`)
131
- }
132
-
133
- function getInlineableBlock(
134
- block: IRBlock | undefined,
135
- currentFn: string,
136
- namespace: string
137
- ): InlineBlock | null {
138
- if (!block) return null
139
- if (block.term.op === 'jump_if' || block.term.op === 'jump_unless' || block.term.op === 'tick_yield') {
140
- return null
141
- }
142
-
143
- const commands = block.instrs.flatMap(instr => emitInstr(instr, namespace))
144
- if (commands.some(command => isRecursiveCommand(command.cmd, currentFn, namespace))) {
145
- return null
146
- }
147
-
148
- if (block.term.op === 'return') {
149
- commands.push(...emitReturn(block.term))
150
- }
151
-
152
- if (commands.length > INLINE_THRESHOLD) {
153
- return null
154
- }
155
-
156
- return {
157
- commands,
158
- continuation: block.term.op === 'jump' ? block.term.target : undefined,
159
- }
160
- }
161
-
162
- function flattenBlock(
163
- fn: IRFunction,
164
- label: string,
165
- namespace: string,
166
- visited: Set<string>
167
- ): IRCommand[] {
168
- const blockMap = new Map(fn.blocks.map(block => [block.label, block]))
169
- const block = blockMap.get(label)
170
- if (!block) {
171
- return []
172
- }
173
-
174
- if (visited.has(label)) {
175
- return [{ cmd: `function ${namespace}:${fn.name}/${label}`, label }]
176
- }
177
-
178
- visited.add(label)
179
-
180
- const commands: IRCommand[] = []
181
- if (label === fn.blocks[0]?.label) {
182
- for (let i = 0; i < fn.params.length; i++) {
183
- commands.push({
184
- cmd: `scoreboard players operation ${varRef(fn.params[i])} ${OBJ} = $p${i} ${OBJ}`,
185
- })
186
- }
187
- }
188
- commands.push(...block.instrs.flatMap(instr => emitInstr(instr, namespace)))
189
- const term = block.term
190
-
191
- switch (term.op) {
192
- case 'jump':
193
- commands.push(...flattenBlock(fn, term.target, namespace, visited))
194
- return commands
195
-
196
- case 'jump_if':
197
- case 'jump_unless': {
198
- const trueLabel = term.op === 'jump_if' ? term.then : term.else_
199
- const falseLabel = term.op === 'jump_if' ? term.else_ : term.then
200
- const trueRange = term.op === 'jump_if' ? '1..' : '..0'
201
- const falseRange = term.op === 'jump_if' ? '..0' : '1..'
202
- const trueBlock = getInlineableBlock(blockMap.get(trueLabel), fn.name, namespace)
203
- const falseBlock = getInlineableBlock(blockMap.get(falseLabel), fn.name, namespace)
204
-
205
- if (trueBlock && falseBlock) {
206
- if (trueBlock.commands.length > 0) {
207
- commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${trueRange}`, label: trueLabel })
208
- commands.push(...markConditional(trueBlock.commands))
209
- }
210
- if (falseBlock.commands.length > 0) {
211
- commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${falseRange}`, label: falseLabel })
212
- commands.push(...markConditional(falseBlock.commands))
213
- }
214
-
215
- const continuation = trueBlock.continuation && trueBlock.continuation === falseBlock.continuation
216
- ? trueBlock.continuation
217
- : undefined
218
- if (continuation) {
219
- commands.push(...flattenBlock(fn, continuation, namespace, cloneVisited(visited)))
220
- }
221
- return commands
222
- }
223
-
224
- commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${trueRange} run function ${namespace}:${fn.name}/${trueLabel}` })
225
- commands.push({ cmd: `execute if score ${varRef(term.cond)} ${OBJ} matches ${falseRange} run function ${namespace}:${fn.name}/${falseLabel}` })
226
- return commands
227
- }
228
-
229
- case 'return':
230
- commands.push(...emitReturn(term))
231
- return commands
232
-
233
- case 'tick_yield':
234
- commands.push({ cmd: `schedule function ${namespace}:${fn.name}/${term.continuation} 1t replace` })
235
- return commands
236
- }
237
- }
238
-
239
- function findVars(command: string): string[] {
240
- return Array.from(command.matchAll(/\$[A-Za-z0-9_]+/g), match => match[0])
241
- }
242
-
243
- function parsePureWrite(command: string): { dst: string; reads: string[] } | null {
244
- let match = command.match(/^scoreboard players set (\$[A-Za-z0-9_]+) rs -?\d+$/)
245
- if (match) {
246
- return { dst: match[1], reads: [] }
247
- }
248
-
249
- match = command.match(/^scoreboard players operation (\$[A-Za-z0-9_]+) rs = (\$[A-Za-z0-9_]+) rs$/)
250
- if (match) {
251
- return { dst: match[1], reads: [match[2]] }
252
- }
253
-
254
- match = command.match(/^execute .* run scoreboard players set (\$[A-Za-z0-9_]+) rs -?\d+$/)
255
- if (match) {
256
- return {
257
- dst: match[1],
258
- reads: findVars(command).filter(name => name !== match![1]),
259
- }
260
- }
261
-
262
- return null
263
- }
264
-
265
- function deadStoreEliminate(commands: IRCommand[]): IRCommand[] {
266
- const live = new Set<string>()
267
- const kept: IRCommand[] = []
268
-
269
- for (let i = commands.length - 1; i >= 0; i--) {
270
- const command = commands[i]
271
- const pureWrite = parsePureWrite(command.cmd)
272
-
273
- if (pureWrite) {
274
- pureWrite.reads.forEach(name => live.add(name))
275
- if (!live.has(pureWrite.dst)) {
276
- continue
277
- }
278
- live.delete(pureWrite.dst)
279
- kept.push(command)
280
- continue
281
- }
282
-
283
- findVars(command.cmd).forEach(name => live.add(name))
284
- kept.push(command)
285
- }
286
-
287
- return kept.reverse()
288
- }
289
-
290
- function isInlineableFunction(
291
- fn: IRFunction | undefined,
292
- currentFn: string,
293
- namespace: string
294
- ): fn is IRFunction & { commands: IRCommand[] } {
295
- if (!fn?.commands || fn.name === currentFn || fn.commands.length > INLINE_THRESHOLD) {
296
- return false
297
- }
298
-
299
- return !fn.commands.some(command =>
300
- isRecursiveCommand(command.cmd, currentFn, namespace) ||
301
- isRecursiveCommand(command.cmd, fn.name, namespace)
302
- )
303
- }
304
-
305
- function inlineConditionalCalls(
306
- commands: IRCommand[],
307
- functions: Map<string, IRFunction>,
308
- currentFn: string,
309
- namespace: string
310
- ): IRCommand[] {
311
- const optimized: IRCommand[] = []
312
-
313
- for (const command of commands) {
314
- const match = command.cmd.match(/^(execute .+) run function ([^:]+):(.+)$/)
315
- if (!match || match[2] !== namespace) {
316
- optimized.push(command)
317
- continue
318
- }
319
-
320
- const target = functions.get(match[3])
321
- if (!isInlineableFunction(target, currentFn, namespace)) {
322
- optimized.push(command)
323
- continue
324
- }
325
-
326
- optimized.push({ cmd: match[1], label: command.label })
327
- optimized.push(...markConditional(target.commands))
328
- }
329
-
330
- return optimized
331
- }
332
-
333
- function invertExecuteCondition(command: string): string | null {
334
- if (command.startsWith('execute if ')) {
335
- return command.replace(/^execute if /, 'execute unless ')
336
- }
337
- if (command.startsWith('execute unless ')) {
338
- return command.replace(/^execute unless /, 'execute if ')
339
- }
340
- return null
341
- }
342
-
343
- function eliminateBranchVariables(
344
- commands: IRCommand[],
345
- functions: Map<string, IRFunction>,
346
- currentFn: string,
347
- namespace: string
348
- ): IRCommand[] {
349
- const optimized: IRCommand[] = []
350
-
351
- for (let i = 0; i < commands.length; i++) {
352
- const init = commands[i]
353
- const set = commands[i + 1]
354
- const thenCmd = commands[i + 2]
355
- const elseCmd = commands[i + 3]
356
-
357
- const initMatch = init?.cmd.match(/^scoreboard players set (\$[A-Za-z0-9_]+) rs 0$/)
358
- const setMatch = set?.cmd.match(/^((?:execute if|execute unless) .+) run scoreboard players set (\$[A-Za-z0-9_]+) rs 1$/)
359
- const thenMatch = thenCmd?.cmd.match(/^execute if score (\$[A-Za-z0-9_]+) rs matches 1\.\. run function [^:]+:(.+)$/)
360
- const elseMatch =
361
- elseCmd?.cmd.match(/^execute if score (\$[A-Za-z0-9_]+) rs matches ..0 run function [^:]+:(.+)$/) ??
362
- elseCmd?.cmd.match(/^execute unless score (\$[A-Za-z0-9_]+) rs matches 1\.\. run function [^:]+:(.+)$/)
363
-
364
- if (!initMatch || !setMatch || !thenMatch || !elseMatch) {
365
- optimized.push(init)
366
- continue
367
- }
368
-
369
- const branchVar = initMatch[1]
370
- if (setMatch[2] !== branchVar || thenMatch[1] !== branchVar || elseMatch[1] !== branchVar) {
371
- optimized.push(init)
372
- continue
373
- }
374
-
375
- const thenFn = functions.get(thenMatch[2])
376
- const elseFn = functions.get(elseMatch[2])
377
- if (!isInlineableFunction(thenFn, currentFn, namespace) || !isInlineableFunction(elseFn, currentFn, namespace)) {
378
- optimized.push(init)
379
- continue
380
- }
381
-
382
- const thenCondition = setMatch[1]
383
- const elseCondition = invertExecuteCondition(thenCondition)
384
- if (!elseCondition) {
385
- optimized.push(init)
386
- continue
387
- }
388
-
389
- optimized.push({ cmd: thenCondition })
390
- optimized.push(...markConditional(thenFn.commands))
391
- if (elseFn.commands.length > 0) {
392
- optimized.push({ cmd: elseCondition })
393
- optimized.push(...markConditional(elseFn.commands))
394
- }
395
- i += 3
396
- }
397
-
398
- return optimized
399
- }
400
-
401
- export function optimizeFunctionForStructure(
402
- fn: IRFunction,
403
- functions: Map<string, IRFunction>,
404
- namespace: string
405
- ): IRCommand[] {
406
- if (fn.blocks.length === 0) {
407
- return []
408
- }
409
-
410
- const linear = flattenBlock(fn, fn.blocks[0].label, namespace, new Set<string>())
411
- const branchEliminated = eliminateBranchVariables(linear, functions, fn.name, namespace)
412
- const inlined = inlineConditionalCalls(branchEliminated, functions, fn.name, namespace)
413
- return deadStoreEliminate(inlined)
414
- }
415
-
416
- export function optimizeForStructure(functions: IRFunction[], namespace = 'redscript'): IRFunction[] {
417
- return optimizeForStructureWithStats(functions, namespace).functions
418
- }
419
-
420
- export function optimizeForStructureWithStats(
421
- functions: IRFunction[],
422
- namespace = 'redscript'
423
- ): { functions: IRFunction[]; stats: OptimizationStats } {
424
- const staged = new Map(functions.map(fn => [fn.name, { ...fn }]))
425
-
426
- for (const fn of staged.values()) {
427
- fn.commands = flattenBlock(fn, fn.blocks[0]?.label ?? 'entry', namespace, new Set<string>())
428
- }
429
-
430
- for (const fn of staged.values()) {
431
- fn.commands = optimizeFunctionForStructure(fn, staged, namespace)
432
- }
433
-
434
- const optimizedCommands = optimizeCommandFunctions(
435
- Array.from(staged.values()).map(fn => ({
436
- name: fn.name,
437
- commands: fn.commands ?? [],
438
- }))
439
- )
440
- const stats = createEmptyOptimizationStats()
441
- mergeOptimizationStats(stats, optimizedCommands.stats)
442
-
443
- return {
444
- functions: Array.from(staged.values()).map(fn => ({
445
- ...fn,
446
- commands: optimizedCommands.functions.find(candidate => candidate.name === fn.name)?.commands ?? fn.commands,
447
- })),
448
- stats,
449
- }
450
- }