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
@@ -0,0 +1,1160 @@
1
+ /**
2
+ * HIR → MIR Lowering — Stage 3 of the RedScript compiler pipeline.
3
+ *
4
+ * Converts structured HIR (if/while/break/continue) into an explicit CFG
5
+ * with 3-address instructions and unlimited fresh temporaries.
6
+ */
7
+
8
+ import type {
9
+ HIRModule, HIRFunction, HIRStmt, HIRBlock, HIRExpr,
10
+ HIRExecuteSubcommand, HIRParam,
11
+ } from '../hir/types'
12
+ import type {
13
+ MIRModule, MIRFunction, MIRBlock, MIRInstr, BlockId,
14
+ Operand, Temp, CmpOp, ExecuteSubcmd, NBTType,
15
+ } from './types'
16
+ import { detectMacroFunctions, BUILTIN_SET, type MacroFunctionInfo } from './macro'
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Public API
20
+ // ---------------------------------------------------------------------------
21
+
22
+ export function lowerToMIR(hir: HIRModule): MIRModule {
23
+ // Build struct definitions: name → field names
24
+ const structDefs = new Map<string, string[]>()
25
+ for (const s of hir.structs) {
26
+ structDefs.set(s.name, s.fields.map(f => f.name))
27
+ }
28
+
29
+ // Build impl method info: typeName → methodName → { hasSelf }
30
+ const implMethods = new Map<string, Map<string, { hasSelf: boolean }>>()
31
+ for (const ib of hir.implBlocks) {
32
+ const methods = new Map<string, { hasSelf: boolean }>()
33
+ for (const m of ib.methods) {
34
+ const hasSelf = m.params.length > 0 && m.params[0].name === 'self'
35
+ methods.set(m.name, { hasSelf })
36
+ }
37
+ implMethods.set(ib.typeName, methods)
38
+ }
39
+
40
+ // Pre-scan for macro functions
41
+ const macroInfo = detectMacroFunctions(hir)
42
+
43
+ // Build function param info for call_macro generation at call sites
44
+ const fnParamInfo = new Map<string, HIRParam[]>()
45
+ for (const f of hir.functions) {
46
+ fnParamInfo.set(f.name, f.params)
47
+ }
48
+ for (const ib of hir.implBlocks) {
49
+ for (const m of ib.methods) {
50
+ fnParamInfo.set(`${ib.typeName}::${m.name}`, m.params)
51
+ }
52
+ }
53
+
54
+ const allFunctions: MIRFunction[] = []
55
+ for (const f of hir.functions) {
56
+ const { fn, helpers } = lowerFunction(f, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo)
57
+ allFunctions.push(fn, ...helpers)
58
+ }
59
+
60
+ // Lower impl block methods
61
+ for (const ib of hir.implBlocks) {
62
+ for (const m of ib.methods) {
63
+ const { fn, helpers } = lowerImplMethod(m, ib.typeName, hir.namespace, structDefs, implMethods, macroInfo, fnParamInfo)
64
+ allFunctions.push(fn, ...helpers)
65
+ }
66
+ }
67
+
68
+ return {
69
+ functions: allFunctions,
70
+ namespace: hir.namespace,
71
+ objective: `__${hir.namespace}`,
72
+ }
73
+ }
74
+
75
+ // ---------------------------------------------------------------------------
76
+ // Function lowering context
77
+ // ---------------------------------------------------------------------------
78
+
79
+ class FnContext {
80
+ private tempCounter = 0
81
+ private blockCounter = 0
82
+ readonly blocks: MIRBlock[] = []
83
+ private currentBlock: MIRBlock
84
+ /** Stack of (loopHeader, loopExit, continueTo) for break/continue */
85
+ private loopStack: { header: BlockId; exit: BlockId; continueTo: BlockId }[] = []
86
+ /** Extracted helper functions for execute blocks */
87
+ readonly helperFunctions: MIRFunction[] = []
88
+ private readonly namespace: string
89
+ private readonly fnName: string
90
+ /** Struct definitions: struct name → field names */
91
+ readonly structDefs: Map<string, string[]>
92
+ /** Impl method info: typeName → methodName → { hasSelf } */
93
+ readonly implMethods: Map<string, Map<string, { hasSelf: boolean }>>
94
+ /** Struct variable tracking: varName → { typeName, fields: fieldName → temp } */
95
+ readonly structVars = new Map<string, { typeName: string; fields: Map<string, Temp> }>()
96
+ /** Macro function info for all functions in the module */
97
+ readonly macroInfo: Map<string, MacroFunctionInfo>
98
+ /** Function parameter info for call_macro generation */
99
+ readonly fnParamInfo: Map<string, HIRParam[]>
100
+ /** Macro params for the current function being lowered */
101
+ readonly currentMacroParams: Set<string>
102
+
103
+ constructor(
104
+ namespace: string,
105
+ fnName: string,
106
+ structDefs: Map<string, string[]> = new Map(),
107
+ implMethods: Map<string, Map<string, { hasSelf: boolean }>> = new Map(),
108
+ macroInfo: Map<string, MacroFunctionInfo> = new Map(),
109
+ fnParamInfo: Map<string, HIRParam[]> = new Map(),
110
+ ) {
111
+ this.namespace = namespace
112
+ this.fnName = fnName
113
+ this.structDefs = structDefs
114
+ this.implMethods = implMethods
115
+ this.macroInfo = macroInfo
116
+ this.fnParamInfo = fnParamInfo
117
+ this.currentMacroParams = macroInfo.get(fnName)?.macroParams ?? new Set()
118
+ const entry = this.makeBlock('entry')
119
+ this.currentBlock = entry
120
+ }
121
+
122
+ freshTemp(): Temp {
123
+ return `t${this.tempCounter++}`
124
+ }
125
+
126
+ private makeBlock(id?: string): MIRBlock {
127
+ const block: MIRBlock = {
128
+ id: id ?? `bb${this.blockCounter++}`,
129
+ instrs: [],
130
+ term: { kind: 'return', value: null }, // placeholder
131
+ preds: [],
132
+ }
133
+ this.blocks.push(block)
134
+ return block
135
+ }
136
+
137
+ newBlock(prefix?: string): MIRBlock {
138
+ return this.makeBlock(prefix ? `${prefix}_${this.blockCounter++}` : undefined)
139
+ }
140
+
141
+ emit(instr: MIRInstr): void {
142
+ this.currentBlock.instrs.push(instr)
143
+ }
144
+
145
+ terminate(term: MIRInstr): void {
146
+ this.currentBlock.term = term
147
+ }
148
+
149
+ switchTo(block: MIRBlock): void {
150
+ this.currentBlock = block
151
+ }
152
+
153
+ current(): MIRBlock {
154
+ return this.currentBlock
155
+ }
156
+
157
+ pushLoop(header: BlockId, exit: BlockId, continueTo?: BlockId): void {
158
+ this.loopStack.push({ header, exit, continueTo: continueTo ?? header })
159
+ }
160
+
161
+ popLoop(): void {
162
+ this.loopStack.pop()
163
+ }
164
+
165
+ currentLoop(): { header: BlockId; exit: BlockId; continueTo: BlockId } | undefined {
166
+ return this.loopStack[this.loopStack.length - 1]
167
+ }
168
+
169
+ getNamespace(): string {
170
+ return this.namespace
171
+ }
172
+
173
+ getFnName(): string {
174
+ return this.fnName
175
+ }
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Function lowering
180
+ // ---------------------------------------------------------------------------
181
+
182
+ function lowerFunction(
183
+ fn: HIRFunction,
184
+ namespace: string,
185
+ structDefs: Map<string, string[]> = new Map(),
186
+ implMethods: Map<string, Map<string, { hasSelf: boolean }>> = new Map(),
187
+ macroInfo: Map<string, MacroFunctionInfo> = new Map(),
188
+ fnParamInfo: Map<string, HIRParam[]> = new Map(),
189
+ ): { fn: MIRFunction; helpers: MIRFunction[] } {
190
+ const ctx = new FnContext(namespace, fn.name, structDefs, implMethods, macroInfo, fnParamInfo)
191
+ const fnMacroInfo = macroInfo.get(fn.name)
192
+
193
+ // Create temps for parameters
194
+ const params: { name: Temp; isMacroParam: boolean }[] = fn.params.map(p => {
195
+ const t = ctx.freshTemp()
196
+ return { name: t, isMacroParam: fnMacroInfo?.macroParams.has(p.name) ?? false }
197
+ })
198
+
199
+ // Map parameter names to their temps
200
+ const scope = new Map<string, Temp>()
201
+ fn.params.forEach((p, i) => {
202
+ scope.set(p.name, params[i].name)
203
+ })
204
+
205
+ lowerBlock(fn.body, ctx, scope)
206
+
207
+ // If the current block doesn't have a real terminator, add void return
208
+ const cur = ctx.current()
209
+ if (isPlaceholderTerm(cur.term)) {
210
+ ctx.terminate({ kind: 'return', value: null })
211
+ }
212
+
213
+ // Remove unreachable blocks (dead continuations after return/break/continue)
214
+ const reachable = computeReachable(ctx.blocks, 'entry')
215
+ const liveBlocks = ctx.blocks.filter(b => reachable.has(b.id))
216
+
217
+ // Fill predecessor lists
218
+ computePreds(liveBlocks)
219
+
220
+ const result: MIRFunction = {
221
+ name: fn.name,
222
+ params,
223
+ blocks: liveBlocks,
224
+ entry: 'entry',
225
+ isMacro: fnMacroInfo != null,
226
+ }
227
+
228
+ return { fn: result, helpers: ctx.helperFunctions }
229
+ }
230
+
231
+ function lowerImplMethod(
232
+ method: HIRFunction,
233
+ typeName: string,
234
+ namespace: string,
235
+ structDefs: Map<string, string[]>,
236
+ implMethods: Map<string, Map<string, { hasSelf: boolean }>>,
237
+ macroInfo: Map<string, MacroFunctionInfo> = new Map(),
238
+ fnParamInfo: Map<string, HIRParam[]> = new Map(),
239
+ ): { fn: MIRFunction; helpers: MIRFunction[] } {
240
+ const fnName = `${typeName}::${method.name}`
241
+ const ctx = new FnContext(namespace, fnName, structDefs, implMethods, macroInfo, fnParamInfo)
242
+ const fields = structDefs.get(typeName) ?? []
243
+ const hasSelf = method.params.length > 0 && method.params[0].name === 'self'
244
+
245
+ const params: { name: Temp; isMacroParam: boolean }[] = []
246
+ const scope = new Map<string, Temp>()
247
+
248
+ if (hasSelf) {
249
+ // Self fields become the first N params (one per struct field)
250
+ const selfFields = new Map<string, Temp>()
251
+ for (const fieldName of fields) {
252
+ const t = ctx.freshTemp()
253
+ params.push({ name: t, isMacroParam: false })
254
+ selfFields.set(fieldName, t)
255
+ }
256
+ ctx.structVars.set('self', { typeName, fields: selfFields })
257
+ // Remaining params (after self)
258
+ for (let i = 1; i < method.params.length; i++) {
259
+ const t = ctx.freshTemp()
260
+ params.push({ name: t, isMacroParam: false })
261
+ scope.set(method.params[i].name, t)
262
+ }
263
+ } else {
264
+ // Static method — regular params
265
+ for (const p of method.params) {
266
+ const t = ctx.freshTemp()
267
+ params.push({ name: t, isMacroParam: false })
268
+ scope.set(p.name, t)
269
+ }
270
+ }
271
+
272
+ lowerBlock(method.body, ctx, scope)
273
+
274
+ const cur = ctx.current()
275
+ if (isPlaceholderTerm(cur.term)) {
276
+ ctx.terminate({ kind: 'return', value: null })
277
+ }
278
+
279
+ const reachable = computeReachable(ctx.blocks, 'entry')
280
+ const liveBlocks = ctx.blocks.filter(b => reachable.has(b.id))
281
+ computePreds(liveBlocks)
282
+
283
+ const result: MIRFunction = {
284
+ name: fnName,
285
+ params,
286
+ blocks: liveBlocks,
287
+ entry: 'entry',
288
+ isMacro: macroInfo.has(fnName),
289
+ }
290
+
291
+ return { fn: result, helpers: ctx.helperFunctions }
292
+ }
293
+
294
+ function isPlaceholderTerm(term: MIRInstr): boolean {
295
+ // Our placeholder is a return null that was set in makeBlock
296
+ return term.kind === 'return' && (term as any).value === null
297
+ }
298
+
299
+ function computeReachable(blocks: MIRBlock[], entry: BlockId): Set<BlockId> {
300
+ const reachable = new Set<BlockId>()
301
+ const queue: BlockId[] = [entry]
302
+ while (queue.length > 0) {
303
+ const id = queue.shift()!
304
+ if (reachable.has(id)) continue
305
+ reachable.add(id)
306
+ const block = blocks.find(b => b.id === id)
307
+ if (block) {
308
+ for (const t of getTermTargets(block.term)) {
309
+ if (!reachable.has(t)) queue.push(t)
310
+ }
311
+ }
312
+ }
313
+ return reachable
314
+ }
315
+
316
+ function computePreds(blocks: MIRBlock[]): void {
317
+ // Clear all preds
318
+ for (const b of blocks) b.preds = []
319
+
320
+ for (const b of blocks) {
321
+ const targets = getTermTargets(b.term)
322
+ for (const t of targets) {
323
+ const target = blocks.find(bb => bb.id === t)
324
+ if (target && !target.preds.includes(b.id)) {
325
+ target.preds.push(b.id)
326
+ }
327
+ }
328
+ }
329
+ }
330
+
331
+ function getTermTargets(term: MIRInstr): BlockId[] {
332
+ switch (term.kind) {
333
+ case 'jump': return [term.target]
334
+ case 'branch': return [term.then, term.else]
335
+ case 'return': return []
336
+ default: return []
337
+ }
338
+ }
339
+
340
+ // ---------------------------------------------------------------------------
341
+ // Block / statement lowering
342
+ // ---------------------------------------------------------------------------
343
+
344
+ function lowerBlock(
345
+ stmts: HIRBlock,
346
+ ctx: FnContext,
347
+ scope: Map<string, Temp>,
348
+ ): void {
349
+ for (const stmt of stmts) {
350
+ lowerStmt(stmt, ctx, scope)
351
+ }
352
+ }
353
+
354
+ function lowerStmt(
355
+ stmt: HIRStmt,
356
+ ctx: FnContext,
357
+ scope: Map<string, Temp>,
358
+ ): void {
359
+ switch (stmt.kind) {
360
+ case 'let': {
361
+ if (stmt.init.kind === 'struct_lit') {
362
+ // Struct literal: create per-field temps
363
+ const typeName = (stmt.type?.kind === 'struct') ? stmt.type.name : '__anon'
364
+ const fieldTemps = new Map<string, Temp>()
365
+ for (const field of stmt.init.fields) {
366
+ const val = lowerExpr(field.value, ctx, scope)
367
+ const t = ctx.freshTemp()
368
+ ctx.emit({ kind: 'copy', dst: t, src: val })
369
+ fieldTemps.set(field.name, t)
370
+ }
371
+ ctx.structVars.set(stmt.name, { typeName, fields: fieldTemps })
372
+ } else if (stmt.type?.kind === 'struct') {
373
+ // Struct-typed let with non-literal init (e.g., call returning struct)
374
+ const fields = ctx.structDefs.get(stmt.type.name)
375
+ if (fields) {
376
+ lowerExpr(stmt.init, ctx, scope)
377
+ // Copy from return field slots into struct variable temps
378
+ const fieldTemps = new Map<string, Temp>()
379
+ for (const fieldName of fields) {
380
+ const t = ctx.freshTemp()
381
+ ctx.emit({ kind: 'copy', dst: t, src: { kind: 'temp', name: `__rf_${fieldName}` } })
382
+ fieldTemps.set(fieldName, t)
383
+ }
384
+ ctx.structVars.set(stmt.name, { typeName: stmt.type.name, fields: fieldTemps })
385
+ } else {
386
+ const valOp = lowerExpr(stmt.init, ctx, scope)
387
+ const t = ctx.freshTemp()
388
+ ctx.emit({ kind: 'copy', dst: t, src: valOp })
389
+ scope.set(stmt.name, t)
390
+ }
391
+ } else {
392
+ const valOp = lowerExpr(stmt.init, ctx, scope)
393
+ const t = ctx.freshTemp()
394
+ ctx.emit({ kind: 'copy', dst: t, src: valOp })
395
+ scope.set(stmt.name, t)
396
+ }
397
+ break
398
+ }
399
+
400
+ case 'expr': {
401
+ lowerExpr(stmt.expr, ctx, scope)
402
+ break
403
+ }
404
+
405
+ case 'return': {
406
+ if (stmt.value?.kind === 'struct_lit') {
407
+ // Struct return — copy each field to return field slots
408
+ for (const field of stmt.value.fields) {
409
+ const val = lowerExpr(field.value, ctx, scope)
410
+ ctx.emit({ kind: 'copy', dst: `__rf_${field.name}`, src: val })
411
+ }
412
+ ctx.terminate({ kind: 'return', value: null })
413
+ } else {
414
+ const val = stmt.value ? lowerExpr(stmt.value, ctx, scope) : null
415
+ ctx.terminate({ kind: 'return', value: val })
416
+ }
417
+ // Create a dead block for any subsequent statements
418
+ const dead = ctx.newBlock('post_ret')
419
+ ctx.switchTo(dead)
420
+ break
421
+ }
422
+
423
+ case 'break': {
424
+ const loop = ctx.currentLoop()
425
+ if (!loop) throw new Error('break outside loop')
426
+ ctx.terminate({ kind: 'jump', target: loop.exit })
427
+ const dead = ctx.newBlock('post_break')
428
+ ctx.switchTo(dead)
429
+ break
430
+ }
431
+
432
+ case 'continue': {
433
+ const loop = ctx.currentLoop()
434
+ if (!loop) throw new Error('continue outside loop')
435
+ ctx.terminate({ kind: 'jump', target: loop.continueTo })
436
+ const dead = ctx.newBlock('post_continue')
437
+ ctx.switchTo(dead)
438
+ break
439
+ }
440
+
441
+ case 'if': {
442
+ const condOp = lowerExpr(stmt.cond, ctx, scope)
443
+ const thenBlock = ctx.newBlock('then')
444
+ const mergeBlock = ctx.newBlock('merge')
445
+ const elseBlock = stmt.else_ ? ctx.newBlock('else') : mergeBlock
446
+
447
+ ctx.terminate({ kind: 'branch', cond: condOp, then: thenBlock.id, else: elseBlock.id })
448
+
449
+ // Then branch
450
+ ctx.switchTo(thenBlock)
451
+ lowerBlock(stmt.then, ctx, new Map(scope))
452
+ if (isPlaceholderTerm(ctx.current().term)) {
453
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id })
454
+ }
455
+
456
+ // Else branch
457
+ if (stmt.else_) {
458
+ ctx.switchTo(elseBlock)
459
+ lowerBlock(stmt.else_, ctx, new Map(scope))
460
+ if (isPlaceholderTerm(ctx.current().term)) {
461
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id })
462
+ }
463
+ }
464
+
465
+ ctx.switchTo(mergeBlock)
466
+ break
467
+ }
468
+
469
+ case 'while': {
470
+ const headerBlock = ctx.newBlock('loop_header')
471
+ const bodyBlock = ctx.newBlock('loop_body')
472
+ const exitBlock = ctx.newBlock('loop_exit')
473
+
474
+ // If there's a step block (for/for_range), create a latch block that
475
+ // executes the step and then jumps to the header. Continue targets the
476
+ // latch so the increment always runs.
477
+ let latchBlock: MIRBlock | null = null
478
+ if (stmt.step && stmt.step.length > 0) {
479
+ latchBlock = ctx.newBlock('loop_latch')
480
+ }
481
+ const continueTarget = latchBlock ? latchBlock.id : headerBlock.id
482
+
483
+ // Jump from current block to header
484
+ ctx.terminate({ kind: 'jump', target: headerBlock.id })
485
+
486
+ // Header: evaluate condition
487
+ ctx.switchTo(headerBlock)
488
+ const condOp = lowerExpr(stmt.cond, ctx, scope)
489
+ ctx.terminate({ kind: 'branch', cond: condOp, then: bodyBlock.id, else: exitBlock.id })
490
+
491
+ // Body
492
+ ctx.switchTo(bodyBlock)
493
+ ctx.pushLoop(headerBlock.id, exitBlock.id, continueTarget)
494
+ lowerBlock(stmt.body, ctx, new Map(scope))
495
+ ctx.popLoop()
496
+ if (isPlaceholderTerm(ctx.current().term)) {
497
+ ctx.terminate({ kind: 'jump', target: continueTarget })
498
+ }
499
+
500
+ // Latch block (step): execute increment, then jump to header
501
+ if (latchBlock && stmt.step) {
502
+ ctx.switchTo(latchBlock)
503
+ lowerBlock(stmt.step, ctx, new Map(scope))
504
+ if (isPlaceholderTerm(ctx.current().term)) {
505
+ ctx.terminate({ kind: 'jump', target: headerBlock.id })
506
+ }
507
+ }
508
+
509
+ ctx.switchTo(exitBlock)
510
+ break
511
+ }
512
+
513
+ case 'foreach': {
514
+ // foreach is MC-specific entity iteration — lower to call_context
515
+ // For now, extract body into a helper and emit call_context
516
+ const helperName = `${ctx.getFnName()}__foreach_${ctx.freshTemp()}`
517
+ const subcommands: ExecuteSubcmd[] = []
518
+
519
+ // The iterable should be a selector expression
520
+ if (stmt.iterable.kind === 'selector') {
521
+ subcommands.push({ kind: 'as', selector: stmt.iterable.raw })
522
+ }
523
+ if (stmt.executeContext === '@s') {
524
+ subcommands.push({ kind: 'at_self' })
525
+ }
526
+
527
+ // Build helper function body as MIR
528
+ const helperCtx = new FnContext(ctx.getNamespace(), helperName, ctx.structDefs, ctx.implMethods)
529
+ const helperScope = new Map(scope)
530
+ lowerBlock(stmt.body, helperCtx, helperScope)
531
+ if (isPlaceholderTerm(helperCtx.current().term)) {
532
+ helperCtx.terminate({ kind: 'return', value: null })
533
+ }
534
+ const helperReachable = computeReachable(helperCtx.blocks, 'entry')
535
+ const helperBlocks = helperCtx.blocks.filter(b => helperReachable.has(b.id))
536
+ computePreds(helperBlocks)
537
+
538
+ ctx.helperFunctions.push({
539
+ name: helperName,
540
+ params: [],
541
+ blocks: helperBlocks,
542
+ entry: 'entry',
543
+ isMacro: false,
544
+ })
545
+
546
+ ctx.emit({ kind: 'call_context', fn: helperName, subcommands })
547
+ break
548
+ }
549
+
550
+ case 'execute': {
551
+ // Extract body into a helper function, emit call_context
552
+ const helperName = `${ctx.getFnName()}__exec_${ctx.freshTemp()}`
553
+ const subcommands = stmt.subcommands.map(lowerExecuteSubcmd)
554
+
555
+ const helperCtx = new FnContext(ctx.getNamespace(), helperName, ctx.structDefs, ctx.implMethods)
556
+ const helperScope = new Map(scope)
557
+ lowerBlock(stmt.body, helperCtx, helperScope)
558
+ if (isPlaceholderTerm(helperCtx.current().term)) {
559
+ helperCtx.terminate({ kind: 'return', value: null })
560
+ }
561
+ const execReachable = computeReachable(helperCtx.blocks, 'entry')
562
+ const execBlocks = helperCtx.blocks.filter(b => execReachable.has(b.id))
563
+ computePreds(execBlocks)
564
+
565
+ ctx.helperFunctions.push({
566
+ name: helperName,
567
+ params: [],
568
+ blocks: execBlocks,
569
+ entry: 'entry',
570
+ isMacro: false,
571
+ })
572
+
573
+ ctx.emit({ kind: 'call_context', fn: helperName, subcommands })
574
+ break
575
+ }
576
+
577
+ case 'match': {
578
+ // Lower match as chained if/else
579
+ const matchVal = lowerExpr(stmt.expr, ctx, scope)
580
+ const mergeBlock = ctx.newBlock('match_merge')
581
+
582
+ for (let i = 0; i < stmt.arms.length; i++) {
583
+ const arm = stmt.arms[i]
584
+ if (arm.pattern === null) {
585
+ // Default arm — just emit the body
586
+ lowerBlock(arm.body, ctx, new Map(scope))
587
+ if (isPlaceholderTerm(ctx.current().term)) {
588
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id })
589
+ }
590
+ } else {
591
+ const patOp = lowerExpr(arm.pattern, ctx, scope)
592
+ const cmpTemp = ctx.freshTemp()
593
+ ctx.emit({ kind: 'cmp', dst: cmpTemp, op: 'eq', a: matchVal, b: patOp })
594
+
595
+ const armBody = ctx.newBlock('match_arm')
596
+ const nextArm = ctx.newBlock('match_next')
597
+ ctx.terminate({ kind: 'branch', cond: { kind: 'temp', name: cmpTemp }, then: armBody.id, else: nextArm.id })
598
+
599
+ ctx.switchTo(armBody)
600
+ lowerBlock(arm.body, ctx, new Map(scope))
601
+ if (isPlaceholderTerm(ctx.current().term)) {
602
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id })
603
+ }
604
+
605
+ ctx.switchTo(nextArm)
606
+ }
607
+ }
608
+
609
+ // If no default arm matched, jump to merge
610
+ if (isPlaceholderTerm(ctx.current().term)) {
611
+ ctx.terminate({ kind: 'jump', target: mergeBlock.id })
612
+ }
613
+
614
+ ctx.switchTo(mergeBlock)
615
+ break
616
+ }
617
+
618
+ case 'raw': {
619
+ // Raw commands are opaque at MIR level — emit as a call to a synthetic raw function
620
+ // For now, pass through as a call with no args (will be handled in LIR)
621
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:${stmt.cmd}`, args: [] })
622
+ break
623
+ }
624
+
625
+ default: {
626
+ const _exhaustive: never = stmt
627
+ throw new Error(`Unknown HIR statement kind: ${(_exhaustive as any).kind}`)
628
+ }
629
+ }
630
+ }
631
+
632
+ // ---------------------------------------------------------------------------
633
+ // Expression lowering → produces an Operand (temp or const)
634
+ // ---------------------------------------------------------------------------
635
+
636
+ function lowerExpr(
637
+ expr: HIRExpr,
638
+ ctx: FnContext,
639
+ scope: Map<string, Temp>,
640
+ ): Operand {
641
+ switch (expr.kind) {
642
+ case 'int_lit':
643
+ return { kind: 'const', value: expr.value }
644
+
645
+ case 'float_lit':
646
+ // float is ×1000 fixed-point in RedScript
647
+ return { kind: 'const', value: expr.value }
648
+
649
+ case 'byte_lit':
650
+ case 'short_lit':
651
+ case 'long_lit':
652
+ case 'double_lit':
653
+ return { kind: 'const', value: expr.value }
654
+
655
+ case 'bool_lit': {
656
+ return { kind: 'const', value: expr.value ? 1 : 0 }
657
+ }
658
+
659
+ case 'struct_lit': {
660
+ // Struct literal in expression context (not let/return — those handle it directly).
661
+ // Lower each field value but return a placeholder since the struct
662
+ // is tracked via structVars at the statement level.
663
+ for (const field of expr.fields) {
664
+ lowerExpr(field.value, ctx, scope)
665
+ }
666
+ const t = ctx.freshTemp()
667
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
668
+ return { kind: 'temp', name: t }
669
+ }
670
+
671
+ case 'str_lit':
672
+ case 'range_lit':
673
+ case 'array_lit':
674
+ case 'rel_coord':
675
+ case 'local_coord':
676
+ case 'mc_name':
677
+ case 'blockpos':
678
+ case 'selector':
679
+ case 'str_interp':
680
+ case 'f_string':
681
+ case 'is_check':
682
+ case 'lambda': {
683
+ // MC-specific / complex types — opaque at MIR level
684
+ // Emit as const 0 placeholder; these are handled in LIR lowering
685
+ const t = ctx.freshTemp()
686
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
687
+ return { kind: 'temp', name: t }
688
+ }
689
+
690
+ case 'ident': {
691
+ const temp = scope.get(expr.name)
692
+ if (temp) return { kind: 'temp', name: temp }
693
+ // Unresolved ident — could be a global or external reference
694
+ const t = ctx.freshTemp()
695
+ ctx.emit({ kind: 'copy', dst: t, src: { kind: 'const', value: 0 } })
696
+ scope.set(expr.name, t)
697
+ return { kind: 'temp', name: t }
698
+ }
699
+
700
+ case 'binary': {
701
+ // Handle short-circuit && and ||
702
+ if (expr.op === '&&') {
703
+ return lowerShortCircuitAnd(expr, ctx, scope)
704
+ }
705
+ if (expr.op === '||') {
706
+ return lowerShortCircuitOr(expr, ctx, scope)
707
+ }
708
+
709
+ const left = lowerExpr(expr.left, ctx, scope)
710
+ const right = lowerExpr(expr.right, ctx, scope)
711
+ const t = ctx.freshTemp()
712
+
713
+ // Map HIR binary ops to MIR instructions
714
+ const arithmeticOps: Record<string, MIRInstr['kind']> = {
715
+ '+': 'add', '-': 'sub', '*': 'mul', '/': 'div', '%': 'mod',
716
+ }
717
+ const cmpOps: Record<string, CmpOp> = {
718
+ '==': 'eq', '!=': 'ne', '<': 'lt', '<=': 'le', '>': 'gt', '>=': 'ge',
719
+ }
720
+
721
+ if (expr.op in arithmeticOps) {
722
+ ctx.emit({ kind: arithmeticOps[expr.op] as any, dst: t, a: left, b: right })
723
+ } else if (expr.op in cmpOps) {
724
+ ctx.emit({ kind: 'cmp', dst: t, op: cmpOps[expr.op], a: left, b: right })
725
+ } else {
726
+ throw new Error(`Unknown binary op: ${expr.op}`)
727
+ }
728
+ return { kind: 'temp', name: t }
729
+ }
730
+
731
+ case 'unary': {
732
+ const operand = lowerExpr(expr.operand, ctx, scope)
733
+ const t = ctx.freshTemp()
734
+ if (expr.op === '-') {
735
+ ctx.emit({ kind: 'neg', dst: t, src: operand })
736
+ } else if (expr.op === '!') {
737
+ ctx.emit({ kind: 'not', dst: t, src: operand })
738
+ }
739
+ return { kind: 'temp', name: t }
740
+ }
741
+
742
+ case 'assign': {
743
+ const val = lowerExpr(expr.value, ctx, scope)
744
+ // Reuse the existing temp for this variable so that updates inside
745
+ // if/while bodies are visible to outer code (we target mutable
746
+ // scoreboard slots, not true SSA registers).
747
+ const existing = scope.get(expr.target)
748
+ const t = existing ?? ctx.freshTemp()
749
+ ctx.emit({ kind: 'copy', dst: t, src: val })
750
+ scope.set(expr.target, t)
751
+ return val
752
+ }
753
+
754
+ case 'member_assign': {
755
+ const val = lowerExpr(expr.value, ctx, scope)
756
+ // Struct field assignment: v.x = val → copy val to v's x temp
757
+ if (expr.obj.kind === 'ident') {
758
+ const sv = ctx.structVars.get(expr.obj.name)
759
+ if (sv) {
760
+ const fieldTemp = sv.fields.get(expr.field)
761
+ if (fieldTemp) {
762
+ ctx.emit({ kind: 'copy', dst: fieldTemp, src: val })
763
+ return val
764
+ }
765
+ }
766
+ }
767
+ return val
768
+ }
769
+
770
+ case 'member': {
771
+ // Struct field access: v.x → return v's x temp
772
+ if (expr.obj.kind === 'ident') {
773
+ const sv = ctx.structVars.get(expr.obj.name)
774
+ if (sv) {
775
+ const fieldTemp = sv.fields.get(expr.field)
776
+ if (fieldTemp) return { kind: 'temp', name: fieldTemp }
777
+ }
778
+ }
779
+ // Fallback: opaque
780
+ const obj = lowerExpr(expr.obj, ctx, scope)
781
+ const t = ctx.freshTemp()
782
+ ctx.emit({ kind: 'copy', dst: t, src: obj })
783
+ return { kind: 'temp', name: t }
784
+ }
785
+
786
+ case 'index': {
787
+ const obj = lowerExpr(expr.obj, ctx, scope)
788
+ const idx = lowerExpr(expr.index, ctx, scope)
789
+ const t = ctx.freshTemp()
790
+ ctx.emit({ kind: 'copy', dst: t, src: obj })
791
+ return { kind: 'temp', name: t }
792
+ }
793
+
794
+ case 'call': {
795
+ // Handle builtin calls → raw MC commands
796
+ if (BUILTIN_SET.has(expr.fn)) {
797
+ const cmd = formatBuiltinCall(expr.fn, expr.args, ctx.currentMacroParams)
798
+ ctx.emit({ kind: 'call', dst: null, fn: `__raw:${cmd}`, args: [] })
799
+ const t = ctx.freshTemp()
800
+ ctx.emit({ kind: 'const', dst: t, value: 0 })
801
+ return { kind: 'temp', name: t }
802
+ }
803
+
804
+ // Check for struct instance method call: parser desugars v.method() → call('method', [v, ...])
805
+ if (expr.args.length > 0 && expr.args[0].kind === 'ident') {
806
+ const sv = ctx.structVars.get(expr.args[0].name)
807
+ if (sv) {
808
+ const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.fn)
809
+ if (methodInfo?.hasSelf) {
810
+ // Build args: self fields first, then remaining explicit args
811
+ const fields = ctx.structDefs.get(sv.typeName) ?? []
812
+ const selfArgs: Operand[] = fields.map(f => {
813
+ const temp = sv.fields.get(f)
814
+ return temp ? { kind: 'temp' as const, name: temp } : { kind: 'const' as const, value: 0 }
815
+ })
816
+ const explicitArgs = expr.args.slice(1).map(a => lowerExpr(a, ctx, scope))
817
+ const allArgs = [...selfArgs, ...explicitArgs]
818
+ const t = ctx.freshTemp()
819
+ ctx.emit({ kind: 'call', dst: t, fn: `${sv.typeName}::${expr.fn}`, args: allArgs })
820
+ return { kind: 'temp', name: t }
821
+ }
822
+ }
823
+ }
824
+
825
+ // Check if calling a macro function → emit call_macro
826
+ const targetMacro = ctx.macroInfo.get(expr.fn)
827
+ if (targetMacro) {
828
+ const args = expr.args.map(a => lowerExpr(a, ctx, scope))
829
+ const targetParams = ctx.fnParamInfo.get(expr.fn) ?? []
830
+ const macroArgs: { name: string; value: Operand; type: NBTType; scale: number }[] = []
831
+ for (let i = 0; i < targetParams.length && i < args.length; i++) {
832
+ const paramName = targetParams[i].name
833
+ if (targetMacro.macroParams.has(paramName)) {
834
+ const paramTypeName = targetMacro.paramTypes.get(paramName) ?? 'int'
835
+ const isFloat = paramTypeName === 'float'
836
+ macroArgs.push({
837
+ name: paramName,
838
+ value: args[i],
839
+ type: isFloat ? 'double' : 'int',
840
+ scale: isFloat ? 0.01 : 1,
841
+ })
842
+ }
843
+ }
844
+ const t = ctx.freshTemp()
845
+ ctx.emit({ kind: 'call_macro', dst: t, fn: expr.fn, args: macroArgs })
846
+ return { kind: 'temp', name: t }
847
+ }
848
+
849
+ const args = expr.args.map(a => lowerExpr(a, ctx, scope))
850
+ const t = ctx.freshTemp()
851
+ ctx.emit({ kind: 'call', dst: t, fn: expr.fn, args })
852
+ return { kind: 'temp', name: t }
853
+ }
854
+
855
+ case 'invoke': {
856
+ // Check for struct method call: v.method(args)
857
+ if (expr.callee.kind === 'member' && expr.callee.obj.kind === 'ident') {
858
+ const sv = ctx.structVars.get(expr.callee.obj.name)
859
+ if (sv) {
860
+ const methodInfo = ctx.implMethods.get(sv.typeName)?.get(expr.callee.field)
861
+ if (methodInfo?.hasSelf) {
862
+ // Build args: self fields first, then explicit args
863
+ const fields = ctx.structDefs.get(sv.typeName) ?? []
864
+ const selfArgs: Operand[] = fields.map(f => {
865
+ const temp = sv.fields.get(f)
866
+ return temp ? { kind: 'temp' as const, name: temp } : { kind: 'const' as const, value: 0 }
867
+ })
868
+ const explicitArgs = expr.args.map(a => lowerExpr(a, ctx, scope))
869
+ const allArgs = [...selfArgs, ...explicitArgs]
870
+ const t = ctx.freshTemp()
871
+ ctx.emit({ kind: 'call', dst: t, fn: `${sv.typeName}::${expr.callee.field}`, args: allArgs })
872
+ return { kind: 'temp', name: t }
873
+ }
874
+ }
875
+ }
876
+ // Fallback: generic invoke
877
+ const calleeOp = lowerExpr(expr.callee, ctx, scope)
878
+ const args = expr.args.map(a => lowerExpr(a, ctx, scope))
879
+ const t = ctx.freshTemp()
880
+ ctx.emit({ kind: 'call', dst: t, fn: '__invoke', args: [calleeOp, ...args] })
881
+ return { kind: 'temp', name: t }
882
+ }
883
+
884
+ case 'static_call': {
885
+ const args = expr.args.map(a => lowerExpr(a, ctx, scope))
886
+ const t = ctx.freshTemp()
887
+ ctx.emit({ kind: 'call', dst: t, fn: `${expr.type}::${expr.method}`, args })
888
+ return { kind: 'temp', name: t }
889
+ }
890
+
891
+ default: {
892
+ const _exhaustive: never = expr
893
+ throw new Error(`Unknown HIR expression kind: ${(_exhaustive as any).kind}`)
894
+ }
895
+ }
896
+ }
897
+
898
+ // ---------------------------------------------------------------------------
899
+ // Short-circuit lowering
900
+ // ---------------------------------------------------------------------------
901
+
902
+ function lowerShortCircuitAnd(
903
+ expr: Extract<HIRExpr, { kind: 'binary' }>,
904
+ ctx: FnContext,
905
+ scope: Map<string, Temp>,
906
+ ): Operand {
907
+ // a && b → if(a) { b } else { 0 }
908
+ const left = lowerExpr(expr.left, ctx, scope)
909
+ const result = ctx.freshTemp()
910
+
911
+ const evalRight = ctx.newBlock('and_right')
912
+ const merge = ctx.newBlock('and_merge')
913
+ const falseBlock = ctx.newBlock('and_false')
914
+
915
+ ctx.terminate({ kind: 'branch', cond: left, then: evalRight.id, else: falseBlock.id })
916
+
917
+ ctx.switchTo(evalRight)
918
+ const right = lowerExpr(expr.right, ctx, scope)
919
+ ctx.emit({ kind: 'copy', dst: result, src: right })
920
+ ctx.terminate({ kind: 'jump', target: merge.id })
921
+
922
+ ctx.switchTo(falseBlock)
923
+ ctx.emit({ kind: 'const', dst: result, value: 0 })
924
+ ctx.terminate({ kind: 'jump', target: merge.id })
925
+
926
+ ctx.switchTo(merge)
927
+ return { kind: 'temp', name: result }
928
+ }
929
+
930
+ function lowerShortCircuitOr(
931
+ expr: Extract<HIRExpr, { kind: 'binary' }>,
932
+ ctx: FnContext,
933
+ scope: Map<string, Temp>,
934
+ ): Operand {
935
+ // a || b → if(a) { 1 } else { b }
936
+ const left = lowerExpr(expr.left, ctx, scope)
937
+ const result = ctx.freshTemp()
938
+
939
+ const trueBlock = ctx.newBlock('or_true')
940
+ const evalRight = ctx.newBlock('or_right')
941
+ const merge = ctx.newBlock('or_merge')
942
+
943
+ ctx.terminate({ kind: 'branch', cond: left, then: trueBlock.id, else: evalRight.id })
944
+
945
+ ctx.switchTo(trueBlock)
946
+ ctx.emit({ kind: 'const', dst: result, value: 1 })
947
+ ctx.terminate({ kind: 'jump', target: merge.id })
948
+
949
+ ctx.switchTo(evalRight)
950
+ const right = lowerExpr(expr.right, ctx, scope)
951
+ ctx.emit({ kind: 'copy', dst: result, src: right })
952
+ ctx.terminate({ kind: 'jump', target: merge.id })
953
+
954
+ ctx.switchTo(merge)
955
+ return { kind: 'temp', name: result }
956
+ }
957
+
958
+ // ---------------------------------------------------------------------------
959
+ // Execute subcommand lowering
960
+ // ---------------------------------------------------------------------------
961
+
962
+ function lowerExecuteSubcmd(sub: HIRExecuteSubcommand): ExecuteSubcmd {
963
+ switch (sub.kind) {
964
+ case 'as':
965
+ return { kind: 'as', selector: selectorToString(sub.selector) }
966
+ case 'at':
967
+ return { kind: 'at', selector: selectorToString(sub.selector) }
968
+ case 'positioned':
969
+ return { kind: 'positioned', x: sub.x, y: sub.y, z: sub.z }
970
+ case 'rotated':
971
+ return { kind: 'rotated', yaw: sub.yaw, pitch: sub.pitch }
972
+ case 'in':
973
+ return { kind: 'in', dimension: sub.dimension }
974
+ case 'anchored':
975
+ return { kind: 'anchored', anchor: sub.anchor }
976
+ case 'positioned_as':
977
+ return { kind: 'at', selector: selectorToString(sub.selector) }
978
+ case 'rotated_as':
979
+ return { kind: 'rotated', yaw: '0', pitch: '0' }
980
+ case 'facing':
981
+ return { kind: 'positioned', x: sub.x, y: sub.y, z: sub.z }
982
+ case 'facing_entity':
983
+ return { kind: 'at', selector: selectorToString(sub.selector) }
984
+ case 'align':
985
+ return { kind: 'positioned', x: '0', y: '0', z: '0' }
986
+ case 'on':
987
+ return { kind: 'at_self' }
988
+ case 'summon':
989
+ return { kind: 'at_self' }
990
+ case 'if_entity':
991
+ case 'unless_entity':
992
+ case 'if_block':
993
+ case 'unless_block':
994
+ case 'if_score':
995
+ case 'unless_score':
996
+ case 'if_score_range':
997
+ case 'unless_score_range':
998
+ case 'store_result':
999
+ case 'store_success':
1000
+ // These are condition subcommands — pass through as-is for now
1001
+ return { kind: 'at_self' }
1002
+ default: {
1003
+ const _exhaustive: never = sub
1004
+ throw new Error(`Unknown execute subcommand kind: ${(_exhaustive as any).kind}`)
1005
+ }
1006
+ }
1007
+ }
1008
+
1009
+ function selectorToString(sel: { kind: string; filters?: any }): string {
1010
+ // EntitySelector has kind like '@a', '@e', '@s', etc.
1011
+ // Filters are key=value pairs that become [key=value,key=value]
1012
+ if (!sel.filters || Object.keys(sel.filters).length === 0) {
1013
+ return sel.kind
1014
+ }
1015
+ const parts: string[] = []
1016
+ for (const [key, value] of Object.entries(sel.filters)) {
1017
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
1018
+ // Range filter: { min, max } → key=min..max
1019
+ const rangeObj = value as { min?: number; max?: number }
1020
+ if (rangeObj.min !== undefined && rangeObj.max !== undefined) {
1021
+ parts.push(`${key}=${rangeObj.min}..${rangeObj.max}`)
1022
+ } else if (rangeObj.max !== undefined) {
1023
+ parts.push(`${key}=..${rangeObj.max}`)
1024
+ } else if (rangeObj.min !== undefined) {
1025
+ parts.push(`${key}=${rangeObj.min}..`)
1026
+ }
1027
+ } else {
1028
+ parts.push(`${key}=${value}`)
1029
+ }
1030
+ }
1031
+ return `${sel.kind}[${parts.join(',')}]`
1032
+ }
1033
+
1034
+ // ---------------------------------------------------------------------------
1035
+ // Builtin call formatting → raw MC command strings
1036
+ // ---------------------------------------------------------------------------
1037
+
1038
+ const MACRO_SENTINEL = '\x01'
1039
+
1040
+ /**
1041
+ * Format a builtin call as a raw MC command string.
1042
+ * If any argument uses a macro param, the command is prefixed with \x01
1043
+ * (converted to $ in LIR emission).
1044
+ */
1045
+ function formatBuiltinCall(
1046
+ fn: string,
1047
+ args: HIRExpr[],
1048
+ macroParams: Set<string>,
1049
+ ): string {
1050
+ const fmtArgs = args.map(a => exprToCommandArg(a, macroParams))
1051
+ const strs = fmtArgs.map(a => a.str)
1052
+ const hasMacro = fmtArgs.some(a => a.isMacro)
1053
+
1054
+ let cmd: string
1055
+ switch (fn) {
1056
+ case 'summon': {
1057
+ const [type, x, y, z, nbt] = strs
1058
+ const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
1059
+ cmd = nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
1060
+ break
1061
+ }
1062
+ case 'particle': {
1063
+ const [name, x, y, z, ...rest] = strs
1064
+ const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
1065
+ const extra = rest.filter(v => v !== undefined)
1066
+ cmd = extra.length > 0 ? `particle ${name} ${pos} ${extra.join(' ')}` : `particle ${name} ${pos}`
1067
+ break
1068
+ }
1069
+ case 'setblock': {
1070
+ const [x, y, z, block] = strs
1071
+ cmd = `setblock ${x} ${y} ${z} ${block}`
1072
+ break
1073
+ }
1074
+ case 'fill': {
1075
+ const [x1, y1, z1, x2, y2, z2, block] = strs
1076
+ cmd = `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`
1077
+ break
1078
+ }
1079
+ case 'say': cmd = `say ${strs[0] ?? ''}`; break
1080
+ case 'tell':
1081
+ case 'tellraw': cmd = `tellraw ${strs[0]} {"text":"${strs[1]}"}`; break
1082
+ case 'title': cmd = `title ${strs[0]} title {"text":"${strs[1]}"}`; break
1083
+ case 'actionbar': cmd = `title ${strs[0]} actionbar {"text":"${strs[1]}"}`; break
1084
+ case 'subtitle': cmd = `title ${strs[0]} subtitle {"text":"${strs[1]}"}`; break
1085
+ case 'title_times': cmd = `title ${strs[0]} times ${strs[1]} ${strs[2]} ${strs[3]}`; break
1086
+ case 'announce': cmd = `tellraw @a {"text":"${strs[0]}"}`; break
1087
+ case 'give': {
1088
+ const nbt = strs[3] ? strs[3] : ''
1089
+ cmd = `give ${strs[0]} ${strs[1]}${nbt} ${strs[2] ?? '1'}`
1090
+ break
1091
+ }
1092
+ case 'kill': cmd = `kill ${strs[0] ?? '@s'}`; break
1093
+ case 'effect': cmd = `effect give ${strs[0]} ${strs[1]} ${strs[2] ?? '30'} ${strs[3] ?? '0'}`; break
1094
+ case 'effect_clear': cmd = strs[1] ? `effect clear ${strs[0]} ${strs[1]}` : `effect clear ${strs[0]}`; break
1095
+ case 'playsound': cmd = ['playsound', ...strs].filter(Boolean).join(' '); break
1096
+ case 'clear': cmd = `clear ${strs[0]} ${strs[1] ?? ''}`.trim(); break
1097
+ case 'weather': cmd = `weather ${strs[0]}`; break
1098
+ case 'time_set': cmd = `time set ${strs[0]}`; break
1099
+ case 'time_add': cmd = `time add ${strs[0]}`; break
1100
+ case 'gamerule': cmd = `gamerule ${strs[0]} ${strs[1]}`; break
1101
+ case 'tag_add': cmd = `tag ${strs[0]} add ${strs[1]}`; break
1102
+ case 'tag_remove': cmd = `tag ${strs[0]} remove ${strs[1]}`; break
1103
+ case 'kick': cmd = `kick ${strs[0]} ${strs[1] ?? ''}`.trim(); break
1104
+ case 'clone': cmd = `clone ${strs.join(' ')}`; break
1105
+ case 'difficulty': cmd = `difficulty ${strs[0]}`; break
1106
+ case 'xp_add': cmd = `xp add ${strs[0]} ${strs[1]} ${strs[2] ?? 'points'}`; break
1107
+ case 'xp_set': cmd = `xp set ${strs[0]} ${strs[1]} ${strs[2] ?? 'points'}`; break
1108
+ default: cmd = `${fn} ${strs.join(' ')}`
1109
+ }
1110
+
1111
+ return hasMacro ? `${MACRO_SENTINEL}${cmd}` : cmd
1112
+ }
1113
+
1114
+ /** Convert an HIR expression to its MC command string representation */
1115
+ function exprToCommandArg(
1116
+ expr: HIRExpr,
1117
+ macroParams: Set<string>,
1118
+ ): { str: string; isMacro: boolean } {
1119
+ switch (expr.kind) {
1120
+ case 'int_lit': return { str: String(expr.value), isMacro: false }
1121
+ case 'float_lit': return { str: String(expr.value), isMacro: false }
1122
+ case 'byte_lit': return { str: String(expr.value), isMacro: false }
1123
+ case 'short_lit': return { str: String(expr.value), isMacro: false }
1124
+ case 'long_lit': return { str: String(expr.value), isMacro: false }
1125
+ case 'double_lit': return { str: String(expr.value), isMacro: false }
1126
+ case 'bool_lit': return { str: expr.value ? 'true' : 'false', isMacro: false }
1127
+ case 'str_lit': return { str: expr.value, isMacro: false }
1128
+ case 'mc_name': return { str: expr.value, isMacro: false }
1129
+ case 'selector': return { str: expr.raw, isMacro: false }
1130
+ case 'ident':
1131
+ if (macroParams.has(expr.name)) return { str: `$(${expr.name})`, isMacro: true }
1132
+ return { str: expr.name, isMacro: false }
1133
+ case 'local_coord': {
1134
+ const prefix = expr.value[0] // ^
1135
+ const rest = expr.value.slice(1)
1136
+ if (rest && /^[a-zA-Z_]\w*$/.test(rest) && macroParams.has(rest)) {
1137
+ return { str: `${prefix}$(${rest})`, isMacro: true }
1138
+ }
1139
+ return { str: expr.value, isMacro: false }
1140
+ }
1141
+ case 'rel_coord': {
1142
+ const prefix = expr.value[0] // ~
1143
+ const rest = expr.value.slice(1)
1144
+ if (rest && /^[a-zA-Z_]\w*$/.test(rest) && macroParams.has(rest)) {
1145
+ return { str: `${prefix}$(${rest})`, isMacro: true }
1146
+ }
1147
+ return { str: expr.value, isMacro: false }
1148
+ }
1149
+ case 'unary':
1150
+ if (expr.op === '-' && expr.operand.kind === 'float_lit') {
1151
+ return { str: String(-expr.operand.value), isMacro: false }
1152
+ }
1153
+ if (expr.op === '-' && expr.operand.kind === 'int_lit') {
1154
+ return { str: String(-expr.operand.value), isMacro: false }
1155
+ }
1156
+ return { str: '~', isMacro: false }
1157
+ default:
1158
+ return { str: '~', isMacro: false }
1159
+ }
1160
+ }