redscript-mc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/publish-extension.yml +35 -0
  7. package/LICENSE +21 -0
  8. package/README.md +261 -0
  9. package/README.zh.md +261 -0
  10. package/dist/__tests__/cli.test.d.ts +1 -0
  11. package/dist/__tests__/cli.test.js +140 -0
  12. package/dist/__tests__/codegen.test.d.ts +1 -0
  13. package/dist/__tests__/codegen.test.js +121 -0
  14. package/dist/__tests__/diagnostics.test.d.ts +4 -0
  15. package/dist/__tests__/diagnostics.test.js +149 -0
  16. package/dist/__tests__/e2e.test.d.ts +6 -0
  17. package/dist/__tests__/e2e.test.js +1528 -0
  18. package/dist/__tests__/lexer.test.d.ts +1 -0
  19. package/dist/__tests__/lexer.test.js +316 -0
  20. package/dist/__tests__/lowering.test.d.ts +1 -0
  21. package/dist/__tests__/lowering.test.js +819 -0
  22. package/dist/__tests__/mc-integration.test.d.ts +12 -0
  23. package/dist/__tests__/mc-integration.test.js +395 -0
  24. package/dist/__tests__/mc-syntax.test.d.ts +1 -0
  25. package/dist/__tests__/mc-syntax.test.js +112 -0
  26. package/dist/__tests__/nbt.test.d.ts +1 -0
  27. package/dist/__tests__/nbt.test.js +82 -0
  28. package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
  29. package/dist/__tests__/optimizer-advanced.test.js +124 -0
  30. package/dist/__tests__/optimizer.test.d.ts +1 -0
  31. package/dist/__tests__/optimizer.test.js +118 -0
  32. package/dist/__tests__/parser.test.d.ts +1 -0
  33. package/dist/__tests__/parser.test.js +717 -0
  34. package/dist/__tests__/repl.test.d.ts +1 -0
  35. package/dist/__tests__/repl.test.js +27 -0
  36. package/dist/__tests__/runtime.test.d.ts +1 -0
  37. package/dist/__tests__/runtime.test.js +276 -0
  38. package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
  39. package/dist/__tests__/structure-optimizer.test.js +33 -0
  40. package/dist/__tests__/typechecker.test.d.ts +1 -0
  41. package/dist/__tests__/typechecker.test.js +364 -0
  42. package/dist/ast/types.d.ts +357 -0
  43. package/dist/ast/types.js +9 -0
  44. package/dist/cli.d.ts +11 -0
  45. package/dist/cli.js +407 -0
  46. package/dist/codegen/cmdblock/index.d.ts +26 -0
  47. package/dist/codegen/cmdblock/index.js +45 -0
  48. package/dist/codegen/mcfunction/index.d.ts +34 -0
  49. package/dist/codegen/mcfunction/index.js +413 -0
  50. package/dist/codegen/structure/index.d.ts +18 -0
  51. package/dist/codegen/structure/index.js +249 -0
  52. package/dist/compile.d.ts +30 -0
  53. package/dist/compile.js +152 -0
  54. package/dist/data/arena/function/__load.mcfunction +6 -0
  55. package/dist/data/arena/function/__tick.mcfunction +2 -0
  56. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  57. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  58. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  59. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  60. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  61. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  62. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  63. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  64. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  65. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  66. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  67. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  68. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  69. package/dist/data/counter/function/__load.mcfunction +5 -0
  70. package/dist/data/counter/function/__tick.mcfunction +2 -0
  71. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  72. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  73. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  74. package/dist/data/minecraft/tags/function/load.json +5 -0
  75. package/dist/data/minecraft/tags/function/tick.json +5 -0
  76. package/dist/data/quiz/function/__load.mcfunction +16 -0
  77. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  78. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  79. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  80. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  81. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  82. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  83. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  84. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  85. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  86. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  87. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  88. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  89. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  90. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  91. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  92. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  93. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  94. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  95. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  96. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  97. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  98. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  99. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  100. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  101. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  102. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  103. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  104. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  105. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  106. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  107. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  108. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  109. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  110. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  111. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  112. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  113. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  114. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  115. package/dist/data/shop/function/__load.mcfunction +7 -0
  116. package/dist/data/shop/function/__tick.mcfunction +3 -0
  117. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  118. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  119. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  120. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  121. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  122. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  123. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  124. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  125. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  126. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  127. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  128. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  129. package/dist/data/turret/function/__load.mcfunction +5 -0
  130. package/dist/data/turret/function/__tick.mcfunction +4 -0
  131. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  132. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  133. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  134. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  135. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  136. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  137. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  138. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  139. package/dist/diagnostics/index.d.ts +44 -0
  140. package/dist/diagnostics/index.js +140 -0
  141. package/dist/index.d.ts +53 -0
  142. package/dist/index.js +126 -0
  143. package/dist/ir/builder.d.ts +32 -0
  144. package/dist/ir/builder.js +99 -0
  145. package/dist/ir/types.d.ts +117 -0
  146. package/dist/ir/types.js +15 -0
  147. package/dist/lexer/index.d.ts +36 -0
  148. package/dist/lexer/index.js +458 -0
  149. package/dist/lowering/index.d.ts +106 -0
  150. package/dist/lowering/index.js +2041 -0
  151. package/dist/mc-test/client.d.ts +128 -0
  152. package/dist/mc-test/client.js +174 -0
  153. package/dist/mc-test/runner.d.ts +28 -0
  154. package/dist/mc-test/runner.js +150 -0
  155. package/dist/mc-test/setup.d.ts +11 -0
  156. package/dist/mc-test/setup.js +98 -0
  157. package/dist/mc-validator/index.d.ts +17 -0
  158. package/dist/mc-validator/index.js +322 -0
  159. package/dist/nbt/index.d.ts +86 -0
  160. package/dist/nbt/index.js +250 -0
  161. package/dist/optimizer/commands.d.ts +36 -0
  162. package/dist/optimizer/commands.js +349 -0
  163. package/dist/optimizer/passes.d.ts +34 -0
  164. package/dist/optimizer/passes.js +227 -0
  165. package/dist/optimizer/structure.d.ts +8 -0
  166. package/dist/optimizer/structure.js +344 -0
  167. package/dist/pack.mcmeta +6 -0
  168. package/dist/parser/index.d.ts +76 -0
  169. package/dist/parser/index.js +1193 -0
  170. package/dist/repl.d.ts +16 -0
  171. package/dist/repl.js +165 -0
  172. package/dist/runtime/index.d.ts +101 -0
  173. package/dist/runtime/index.js +1288 -0
  174. package/dist/typechecker/index.d.ts +42 -0
  175. package/dist/typechecker/index.js +629 -0
  176. package/docs/COMPILATION_STATS.md +142 -0
  177. package/docs/IMPLEMENTATION_GUIDE.md +512 -0
  178. package/docs/LANGUAGE_REFERENCE.md +415 -0
  179. package/docs/MC_MAPPING.md +280 -0
  180. package/docs/STRUCTURE_TARGET.md +80 -0
  181. package/docs/mc-reference/commands.md +259 -0
  182. package/editors/vscode/.vscodeignore +10 -0
  183. package/editors/vscode/LICENSE +21 -0
  184. package/editors/vscode/README.md +78 -0
  185. package/editors/vscode/build.mjs +28 -0
  186. package/editors/vscode/icon.png +0 -0
  187. package/editors/vscode/mcfunction-language-configuration.json +28 -0
  188. package/editors/vscode/out/extension.js +7236 -0
  189. package/editors/vscode/package-lock.json +566 -0
  190. package/editors/vscode/package.json +137 -0
  191. package/editors/vscode/redscript-language-configuration.json +28 -0
  192. package/editors/vscode/snippets/redscript.json +114 -0
  193. package/editors/vscode/src/codeactions.ts +89 -0
  194. package/editors/vscode/src/completion.ts +130 -0
  195. package/editors/vscode/src/extension.ts +239 -0
  196. package/editors/vscode/src/hover.ts +1120 -0
  197. package/editors/vscode/src/symbols.ts +207 -0
  198. package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
  199. package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
  200. package/editors/vscode/tsconfig.json +13 -0
  201. package/jest.config.js +5 -0
  202. package/package.json +38 -0
  203. package/src/__tests__/cli.test.ts +130 -0
  204. package/src/__tests__/codegen.test.ts +128 -0
  205. package/src/__tests__/diagnostics.test.ts +195 -0
  206. package/src/__tests__/e2e.test.ts +1721 -0
  207. package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
  208. package/src/__tests__/formatter.test.ts +46 -0
  209. package/src/__tests__/lexer.test.ts +356 -0
  210. package/src/__tests__/lowering.test.ts +962 -0
  211. package/src/__tests__/mc-integration.test.ts +409 -0
  212. package/src/__tests__/mc-syntax.test.ts +96 -0
  213. package/src/__tests__/nbt.test.ts +58 -0
  214. package/src/__tests__/optimizer-advanced.test.ts +144 -0
  215. package/src/__tests__/optimizer.test.ts +129 -0
  216. package/src/__tests__/parser.test.ts +800 -0
  217. package/src/__tests__/repl.test.ts +33 -0
  218. package/src/__tests__/runtime.test.ts +289 -0
  219. package/src/__tests__/structure-optimizer.test.ts +38 -0
  220. package/src/__tests__/typechecker.test.ts +395 -0
  221. package/src/ast/types.ts +248 -0
  222. package/src/cli.ts +445 -0
  223. package/src/codegen/cmdblock/index.ts +63 -0
  224. package/src/codegen/mcfunction/index.ts +471 -0
  225. package/src/codegen/structure/index.ts +305 -0
  226. package/src/compile.ts +188 -0
  227. package/src/diagnostics/index.ts +186 -0
  228. package/src/examples/README.md +77 -0
  229. package/src/examples/SHOWCASE_GAME.md +43 -0
  230. package/src/examples/arena.rs +44 -0
  231. package/src/examples/counter.rs +12 -0
  232. package/src/examples/pvp_arena.rs +131 -0
  233. package/src/examples/quiz.rs +90 -0
  234. package/src/examples/rpg.rs +13 -0
  235. package/src/examples/shop.rs +30 -0
  236. package/src/examples/showcase_game.rs +552 -0
  237. package/src/examples/stdlib_demo.rs +181 -0
  238. package/src/examples/turret.rs +27 -0
  239. package/src/examples/world_manager.rs +23 -0
  240. package/src/formatter/index.ts +22 -0
  241. package/src/index.ts +161 -0
  242. package/src/ir/builder.ts +114 -0
  243. package/src/ir/types.ts +119 -0
  244. package/src/lexer/index.ts +555 -0
  245. package/src/lowering/index.ts +2406 -0
  246. package/src/mc-test/client.ts +259 -0
  247. package/src/mc-test/runner.ts +140 -0
  248. package/src/mc-test/setup.ts +70 -0
  249. package/src/mc-validator/index.ts +367 -0
  250. package/src/nbt/index.ts +321 -0
  251. package/src/optimizer/commands.ts +416 -0
  252. package/src/optimizer/passes.ts +233 -0
  253. package/src/optimizer/structure.ts +441 -0
  254. package/src/parser/index.ts +1437 -0
  255. package/src/repl.ts +165 -0
  256. package/src/runtime/index.ts +1403 -0
  257. package/src/stdlib/README.md +156 -0
  258. package/src/stdlib/combat.rs +20 -0
  259. package/src/stdlib/cooldown.rs +45 -0
  260. package/src/stdlib/math.rs +49 -0
  261. package/src/stdlib/mobs.rs +99 -0
  262. package/src/stdlib/player.rs +29 -0
  263. package/src/stdlib/strings.rs +7 -0
  264. package/src/stdlib/timer.rs +51 -0
  265. package/src/templates/README.md +126 -0
  266. package/src/templates/combat.rs +96 -0
  267. package/src/templates/economy.rs +40 -0
  268. package/src/templates/mini-game-framework.rs +117 -0
  269. package/src/templates/quest.rs +78 -0
  270. package/src/test_programs/zombie_game.rs +25 -0
  271. package/src/typechecker/index.ts +737 -0
  272. package/tsconfig.json +16 -0
@@ -0,0 +1,2406 @@
1
+ /**
2
+ * RedScript Lowering
3
+ *
4
+ * Transforms AST into IR (Three-Address Code).
5
+ * Handles control flow, function extraction for foreach, and builtin calls.
6
+ */
7
+
8
+ import type { IRBuilder } from '../ir/builder'
9
+ import { buildModule } from '../ir/builder'
10
+ import type { IRFunction, IRModule, Operand, BinOp, CmpOp } from '../ir/types'
11
+ import { DiagnosticError } from '../diagnostics'
12
+ import type {
13
+ Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, Program, RangeExpr, Span, Stmt,
14
+ StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent
15
+ } from '../ast/types'
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Builtin Functions
19
+ // ---------------------------------------------------------------------------
20
+
21
+ const BUILTINS: Record<string, (args: string[]) => string | null> = {
22
+ say: ([msg]) => `say ${msg}`,
23
+ tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
24
+ title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
25
+ actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
26
+ subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
27
+ title_times: ([sel, fadeIn, stay, fadeOut]) => `title ${sel} times ${fadeIn} ${stay} ${fadeOut}`,
28
+ announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
29
+ give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
30
+ kill: ([sel]) => `kill ${sel ?? '@s'}`,
31
+ effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
32
+ summon: ([type, x, y, z, nbt]) => {
33
+ const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
34
+ return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
35
+ },
36
+ particle: ([name, x, y, z]) => {
37
+ const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
38
+ return `particle ${name} ${pos}`
39
+ },
40
+ playsound: ([sound, source, sel, x, y, z, volume, pitch, minVolume]) =>
41
+ ['playsound', sound, source, sel, x, y, z, volume, pitch, minVolume].filter(Boolean).join(' '),
42
+ tp: () => null, // Special handling
43
+ tp_to: () => null, // Special handling (deprecated alias)
44
+ clear: ([sel, item]) => `clear ${sel} ${item ?? ''}`.trim(),
45
+ weather: ([type]) => `weather ${type}`,
46
+ time_set: ([val]) => `time set ${val}`,
47
+ time_add: ([val]) => `time add ${val}`,
48
+ gamerule: ([rule, val]) => `gamerule ${rule} ${val}`,
49
+ tag_add: ([sel, tag]) => `tag ${sel} add ${tag}`,
50
+ tag_remove: ([sel, tag]) => `tag ${sel} remove ${tag}`,
51
+ kick: ([player, reason]) => `kick ${player} ${reason ?? ''}`.trim(),
52
+ setblock: ([x, y, z, block]) => `setblock ${x} ${y} ${z} ${block}`,
53
+ fill: ([x1, y1, z1, x2, y2, z2, block]) => `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`,
54
+ clone: ([x1, y1, z1, x2, y2, z2, dx, dy, dz]) => `clone ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${dx} ${dy} ${dz}`,
55
+ difficulty: ([level]) => `difficulty ${level}`,
56
+ xp_add: ([sel, amount, type]) => `xp add ${sel} ${amount} ${type ?? 'points'}`,
57
+ xp_set: ([sel, amount, type]) => `xp set ${sel} ${amount} ${type ?? 'points'}`,
58
+ random: () => null, // Special handling
59
+ random_native: () => null, // Special handling
60
+ random_sequence: () => null, // Special handling
61
+ scoreboard_get: () => null, // Special handling (returns value)
62
+ scoreboard_set: () => null, // Special handling
63
+ score: () => null, // Special handling (same as scoreboard_get)
64
+ scoreboard_display: () => null, // Special handling
65
+ scoreboard_hide: () => null, // Special handling
66
+ scoreboard_add_objective: () => null, // Special handling
67
+ scoreboard_remove_objective: () => null, // Special handling
68
+ bossbar_add: () => null, // Special handling
69
+ bossbar_set_value: () => null, // Special handling
70
+ bossbar_set_max: () => null, // Special handling
71
+ bossbar_set_color: () => null, // Special handling
72
+ bossbar_set_style: () => null, // Special handling
73
+ bossbar_set_visible: () => null, // Special handling
74
+ bossbar_set_players: () => null, // Special handling
75
+ bossbar_remove: () => null, // Special handling
76
+ bossbar_get_value: () => null, // Special handling
77
+ team_add: () => null, // Special handling
78
+ team_remove: () => null, // Special handling
79
+ team_join: () => null, // Special handling
80
+ team_leave: () => null, // Special handling
81
+ team_option: () => null, // Special handling
82
+ data_get: () => null, // Special handling (returns value from NBT)
83
+ set_new: () => null, // Special handling (returns set ID)
84
+ set_add: () => null, // Special handling
85
+ set_contains: () => null, // Special handling (returns 1/0)
86
+ set_remove: () => null, // Special handling
87
+ set_clear: () => null, // Special handling
88
+ }
89
+
90
+ export interface Warning {
91
+ message: string
92
+ code: string
93
+ line?: number
94
+ col?: number
95
+ }
96
+
97
+ function getSpan(node: unknown): Span | undefined {
98
+ return (node as { span?: Span } | undefined)?.span
99
+ }
100
+
101
+ const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
102
+ const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
103
+
104
+ function normalizeSelector(selector: string, warnings: Warning[]): string {
105
+ return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
106
+ const trimmed = entityType.trim()
107
+
108
+ if (trimmed.includes(':')) {
109
+ if (!NAMESPACED_ENTITY_TYPE_RE.test(trimmed)) {
110
+ throw new DiagnosticError(
111
+ 'LoweringError',
112
+ `Invalid entity type format: "${trimmed}" (must be namespace:name)`,
113
+ { line: 1, col: 1 }
114
+ )
115
+ }
116
+ return match
117
+ }
118
+
119
+ if (!BARE_ENTITY_TYPE_RE.test(trimmed)) {
120
+ throw new DiagnosticError(
121
+ 'LoweringError',
122
+ `Invalid entity type format: "${trimmed}" (must be namespace:name or bare_name)`,
123
+ { line: 1, col: 1 }
124
+ )
125
+ }
126
+
127
+ warnings.push({
128
+ message: `Unnamespaced entity type "${trimmed}", auto-qualifying to "minecraft:${trimmed}"`,
129
+ code: 'W_UNNAMESPACED_TYPE',
130
+ })
131
+ return `type=minecraft:${trimmed}`
132
+ })
133
+ }
134
+
135
+ function emitCoord(component: CoordComponent): string {
136
+ switch (component.kind) {
137
+ case 'absolute':
138
+ return String(component.value)
139
+ case 'relative':
140
+ return component.offset === 0 ? '~' : `~${component.offset}`
141
+ case 'local':
142
+ return component.offset === 0 ? '^' : `^${component.offset}`
143
+ }
144
+ }
145
+
146
+ function emitBlockPos(pos: BlockPosExpr): string {
147
+ return `${emitCoord(pos.x)} ${emitCoord(pos.y)} ${emitCoord(pos.z)}`
148
+ }
149
+
150
+ // ---------------------------------------------------------------------------
151
+ // Lowering Class
152
+ // ---------------------------------------------------------------------------
153
+
154
+ export class Lowering {
155
+ private namespace: string
156
+ private functions: IRFunction[] = []
157
+ private globals: string[] = []
158
+ private fnDecls: Map<string, FnDecl> = new Map()
159
+ private specializedFunctions: Map<string, string> = new Map()
160
+ private currentFn: string = ''
161
+ private foreachCounter: number = 0
162
+ private lambdaCounter: number = 0
163
+ readonly warnings: Warning[] = []
164
+
165
+ // Builder state for current function
166
+ private builder!: LoweringBuilder
167
+ private varMap: Map<string, string> = new Map()
168
+ private lambdaBindings: Map<string, string> = new Map()
169
+ private currentCallbackBindings: Map<string, string> = new Map()
170
+ private currentContext: { binding?: string } = {}
171
+ private blockPosVars: Map<string, BlockPosExpr> = new Map()
172
+
173
+ // Struct definitions: name → { fieldName: TypeNode }
174
+ private structDefs: Map<string, Map<string, TypeNode>> = new Map()
175
+ private enumDefs: Map<string, Map<string, number>> = new Map()
176
+ private functionDefaults: Map<string, Array<Expr | undefined>> = new Map()
177
+ private constValues: Map<string, ConstDecl['value']> = new Map()
178
+ private stringValues: Map<string, string> = new Map()
179
+ // Variable types: varName → TypeNode
180
+ private varTypes: Map<string, TypeNode> = new Map()
181
+ // Float variables (stored as fixed-point × 1000)
182
+ private floatVars: Set<string> = new Set()
183
+ // World object counter for unique tags
184
+ private worldObjCounter: number = 0
185
+
186
+ constructor(namespace: string) {
187
+ this.namespace = namespace
188
+ }
189
+
190
+ lower(program: Program): IRModule {
191
+ this.namespace = program.namespace
192
+
193
+ // Load struct definitions
194
+ for (const struct of program.structs ?? []) {
195
+ const fields = new Map<string, TypeNode>()
196
+ for (const field of struct.fields) {
197
+ fields.set(field.name, field.type)
198
+ }
199
+ this.structDefs.set(struct.name, fields)
200
+ }
201
+
202
+ for (const enumDecl of program.enums ?? []) {
203
+ const variants = new Map<string, number>()
204
+ for (const variant of enumDecl.variants) {
205
+ variants.set(variant.name, variant.value ?? 0)
206
+ }
207
+ this.enumDefs.set(enumDecl.name, variants)
208
+ }
209
+
210
+ for (const constDecl of program.consts ?? []) {
211
+ this.constValues.set(constDecl.name, constDecl.value)
212
+ this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type))
213
+ }
214
+
215
+ for (const fn of program.declarations) {
216
+ this.fnDecls.set(fn.name, fn)
217
+ this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
218
+ }
219
+
220
+ for (const fn of program.declarations) {
221
+ this.lowerFn(fn)
222
+ }
223
+
224
+ return buildModule(this.namespace, this.functions, this.globals)
225
+ }
226
+
227
+ // -------------------------------------------------------------------------
228
+ // Function Lowering
229
+ // -------------------------------------------------------------------------
230
+
231
+ private lowerFn(
232
+ fn: FnDecl,
233
+ options: {
234
+ name?: string
235
+ callbackBindings?: Map<string, string>
236
+ } = {}
237
+ ): void {
238
+ const loweredName = options.name ?? fn.name
239
+ const callbackBindings = options.callbackBindings ?? new Map<string, string>()
240
+ const runtimeParams = fn.params.filter(param => !callbackBindings.has(param.name))
241
+
242
+ this.currentFn = loweredName
243
+ this.foreachCounter = 0
244
+ this.varMap = new Map()
245
+ this.lambdaBindings = new Map()
246
+ this.currentCallbackBindings = new Map(callbackBindings)
247
+ this.currentContext = {}
248
+ this.blockPosVars = new Map()
249
+ this.stringValues = new Map()
250
+ this.builder = new LoweringBuilder()
251
+
252
+ // Map parameters
253
+ for (const param of runtimeParams) {
254
+ const paramName = param.name
255
+ this.varMap.set(paramName, `$${paramName}`)
256
+ this.varTypes.set(paramName, this.normalizeType(param.type))
257
+ }
258
+ for (const param of fn.params) {
259
+ if (callbackBindings.has(param.name)) {
260
+ this.varTypes.set(param.name, this.normalizeType(param.type))
261
+ }
262
+ }
263
+
264
+ // Start entry block
265
+ this.builder.startBlock('entry')
266
+
267
+ // Copy params from $p0, $p1, ... to named variables
268
+ for (let i = 0; i < runtimeParams.length; i++) {
269
+ const paramName = runtimeParams[i].name
270
+ const varName = `$${paramName}`
271
+ this.builder.emitAssign(varName, { kind: 'var', name: `$p${i}` })
272
+ }
273
+
274
+ // Lower body
275
+ this.lowerBlock(fn.body)
276
+
277
+ // If no explicit return, add void return
278
+ if (!this.builder.isBlockSealed()) {
279
+ this.builder.emitReturn()
280
+ }
281
+
282
+ // Build function
283
+ const isTickLoop = fn.decorators.some(d => d.name === 'tick')
284
+ const tickRate = this.getTickRate(fn.decorators)
285
+
286
+ // Check for trigger handler
287
+ const triggerDec = fn.decorators.find(d => d.name === 'on_trigger')
288
+ const isTriggerHandler = !!triggerDec
289
+ const triggerName = triggerDec?.args?.trigger
290
+
291
+ const irFn = this.builder.build(loweredName, runtimeParams.map(p => `$${p.name}`), isTickLoop)
292
+
293
+ // Add trigger metadata if applicable
294
+ if (isTriggerHandler && triggerName) {
295
+ irFn.isTriggerHandler = true
296
+ irFn.triggerName = triggerName
297
+ }
298
+
299
+ const eventDec = fn.decorators.find(d =>
300
+ d.name === 'on_advancement' ||
301
+ d.name === 'on_craft' ||
302
+ d.name === 'on_death' ||
303
+ d.name === 'on_login' ||
304
+ d.name === 'on_join_team'
305
+ )
306
+ if (eventDec) {
307
+ switch (eventDec.name) {
308
+ case 'on_advancement':
309
+ irFn.eventTrigger = { kind: 'advancement', value: eventDec.args?.advancement }
310
+ break
311
+ case 'on_craft':
312
+ irFn.eventTrigger = { kind: 'craft', value: eventDec.args?.item }
313
+ break
314
+ case 'on_death':
315
+ irFn.eventTrigger = { kind: 'death' }
316
+ break
317
+ case 'on_login':
318
+ irFn.eventTrigger = { kind: 'login' }
319
+ break
320
+ case 'on_join_team':
321
+ irFn.eventTrigger = { kind: 'join_team', value: eventDec.args?.team }
322
+ break
323
+ }
324
+ }
325
+
326
+ // Handle tick rate counter if needed
327
+ if (tickRate && tickRate > 1) {
328
+ this.wrapWithTickRate(irFn, tickRate)
329
+ }
330
+
331
+ this.functions.push(irFn)
332
+ }
333
+
334
+ private getTickRate(decorators: Decorator[]): number | undefined {
335
+ const tickDec = decorators.find(d => d.name === 'tick')
336
+ return tickDec?.args?.rate
337
+ }
338
+
339
+ private wrapWithTickRate(fn: IRFunction, rate: number): void {
340
+ // Add tick counter logic to entry block
341
+ const counterVar = `$__tick_${fn.name}`
342
+ this.globals.push(counterVar)
343
+
344
+ // Prepend counter logic to entry block
345
+ const entry = fn.blocks[0]
346
+ const originalInstrs = [...entry.instrs]
347
+ const originalTerm = entry.term
348
+
349
+ entry.instrs = [
350
+ { op: 'raw', cmd: `scoreboard players add ${counterVar} rs 1` },
351
+ ]
352
+
353
+ // Create conditional jump
354
+ const bodyLabel = 'tick_body'
355
+ const skipLabel = 'tick_skip'
356
+
357
+ entry.term = {
358
+ op: 'jump_if',
359
+ cond: `${counterVar}_check`,
360
+ then: bodyLabel,
361
+ else_: skipLabel,
362
+ }
363
+
364
+ // Add check instruction
365
+ entry.instrs.push({
366
+ op: 'raw',
367
+ cmd: `execute store success score ${counterVar}_check rs if score ${counterVar} rs matches ${rate}..`,
368
+ })
369
+
370
+ // Body block (original logic + counter reset)
371
+ fn.blocks.push({
372
+ label: bodyLabel,
373
+ instrs: [
374
+ { op: 'raw', cmd: `scoreboard players set ${counterVar} rs 0` },
375
+ ...originalInstrs,
376
+ ],
377
+ term: originalTerm,
378
+ })
379
+
380
+ // Skip block (just return)
381
+ fn.blocks.push({
382
+ label: skipLabel,
383
+ instrs: [],
384
+ term: { op: 'return' },
385
+ })
386
+ }
387
+
388
+ // -------------------------------------------------------------------------
389
+ // Statement Lowering
390
+ // -------------------------------------------------------------------------
391
+
392
+ private lowerBlock(stmts: Block): void {
393
+ for (const stmt of stmts) {
394
+ this.lowerStmt(stmt)
395
+ }
396
+ }
397
+
398
+ private lowerStmt(stmt: Stmt): void {
399
+ switch (stmt.kind) {
400
+ case 'let':
401
+ this.lowerLetStmt(stmt)
402
+ break
403
+ case 'expr':
404
+ this.lowerExpr(stmt.expr)
405
+ break
406
+ case 'return':
407
+ this.lowerReturnStmt(stmt)
408
+ break
409
+ case 'if':
410
+ this.lowerIfStmt(stmt)
411
+ break
412
+ case 'while':
413
+ this.lowerWhileStmt(stmt)
414
+ break
415
+ case 'for':
416
+ this.lowerForStmt(stmt)
417
+ break
418
+ case 'foreach':
419
+ this.lowerForeachStmt(stmt)
420
+ break
421
+ case 'for_range':
422
+ this.lowerForRangeStmt(stmt)
423
+ break
424
+ case 'match':
425
+ this.lowerMatchStmt(stmt)
426
+ break
427
+ case 'as_block':
428
+ this.lowerAsBlockStmt(stmt)
429
+ break
430
+ case 'at_block':
431
+ this.lowerAtBlockStmt(stmt)
432
+ break
433
+ case 'as_at':
434
+ this.lowerAsAtStmt(stmt)
435
+ break
436
+ case 'execute':
437
+ this.lowerExecuteStmt(stmt)
438
+ break
439
+ case 'raw':
440
+ this.builder.emitRaw(stmt.cmd)
441
+ break
442
+ }
443
+ }
444
+
445
+ private lowerLetStmt(stmt: Extract<Stmt, { kind: 'let' }>): void {
446
+ const varName = `$${stmt.name}`
447
+ this.varMap.set(stmt.name, varName)
448
+
449
+ // Track variable type
450
+ const declaredType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init)
451
+ if (declaredType) {
452
+ this.varTypes.set(stmt.name, declaredType)
453
+ // Track float variables for fixed-point arithmetic
454
+ if (declaredType.kind === 'named' && declaredType.name === 'float') {
455
+ this.floatVars.add(stmt.name)
456
+ }
457
+ }
458
+
459
+ if (stmt.init.kind === 'lambda') {
460
+ const lambdaName = this.lowerLambdaExpr(stmt.init)
461
+ this.lambdaBindings.set(stmt.name, lambdaName)
462
+ return
463
+ }
464
+
465
+ // Handle struct literal initialization
466
+ if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
467
+ const structName = stmt.type.name.toLowerCase()
468
+ for (const field of stmt.init.fields) {
469
+ const path = `rs:heap ${structName}_${stmt.name}.${field.name}`
470
+ const fieldValue = this.lowerExpr(field.value)
471
+ if (fieldValue.kind === 'const') {
472
+ this.builder.emitRaw(`data modify storage ${path} set value ${fieldValue.value}`)
473
+ } else if (fieldValue.kind === 'var') {
474
+ // Copy from scoreboard to NBT
475
+ this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${fieldValue.name} rs`)
476
+ }
477
+ }
478
+ return
479
+ }
480
+
481
+ // Handle array literal initialization
482
+ if (stmt.init.kind === 'array_lit') {
483
+ // Initialize empty NBT array
484
+ this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} set value []`)
485
+ // Add each element
486
+ for (const elem of stmt.init.elements) {
487
+ const elemValue = this.lowerExpr(elem)
488
+ if (elemValue.kind === 'const') {
489
+ this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} append value ${elemValue.value}`)
490
+ } else if (elemValue.kind === 'var') {
491
+ this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} append value 0`)
492
+ this.builder.emitRaw(`execute store result storage rs:heap ${stmt.name}[-1] int 1 run scoreboard players get ${elemValue.name} rs`)
493
+ }
494
+ }
495
+ return
496
+ }
497
+
498
+ // Handle spawn_object returning entity handle
499
+ if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
500
+ const value = this.lowerExpr(stmt.init)
501
+ // value is the selector like @e[tag=__rs_obj_0,limit=1]
502
+ if (value.kind === 'var' && value.name.startsWith('@e[tag=__rs_obj_')) {
503
+ this.varMap.set(stmt.name, value.name)
504
+ // Mark as entity type for later member access
505
+ this.varTypes.set(stmt.name, { kind: 'named', name: 'void' }) // Marker
506
+ }
507
+ return
508
+ }
509
+
510
+ const blockPosValue = this.resolveBlockPosExpr(stmt.init)
511
+ if (blockPosValue) {
512
+ this.blockPosVars.set(stmt.name, blockPosValue)
513
+ return
514
+ }
515
+
516
+ const stmtType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init)
517
+ if (stmtType?.kind === 'named' && stmtType.name === 'string' && this.storeStringValue(stmt.name, stmt.init)) {
518
+ return
519
+ }
520
+
521
+ const value = this.lowerExpr(stmt.init)
522
+ this.builder.emitAssign(varName, value)
523
+ }
524
+
525
+ private lowerReturnStmt(stmt: Extract<Stmt, { kind: 'return' }>): void {
526
+ if (stmt.value) {
527
+ const value = this.lowerExpr(stmt.value)
528
+ this.builder.emitReturn(value)
529
+ } else {
530
+ this.builder.emitReturn()
531
+ }
532
+ }
533
+
534
+ private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
535
+ const condVar = this.lowerExpr(stmt.cond)
536
+ const condName = this.operandToVar(condVar)
537
+
538
+ const thenLabel = this.builder.freshLabel('then')
539
+ const elseLabel = this.builder.freshLabel('else')
540
+ const mergeLabel = this.builder.freshLabel('merge')
541
+
542
+ this.builder.emitJumpIf(condName, thenLabel, stmt.else_ ? elseLabel : mergeLabel)
543
+
544
+ // Then block
545
+ this.builder.startBlock(thenLabel)
546
+ this.lowerBlock(stmt.then)
547
+ if (!this.builder.isBlockSealed()) {
548
+ this.builder.emitJump(mergeLabel)
549
+ }
550
+
551
+ // Else block (if present)
552
+ if (stmt.else_) {
553
+ this.builder.startBlock(elseLabel)
554
+ this.lowerBlock(stmt.else_)
555
+ if (!this.builder.isBlockSealed()) {
556
+ this.builder.emitJump(mergeLabel)
557
+ }
558
+ }
559
+
560
+ // Merge block
561
+ this.builder.startBlock(mergeLabel)
562
+ }
563
+
564
+ private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
565
+ const checkLabel = this.builder.freshLabel('loop_check')
566
+ const bodyLabel = this.builder.freshLabel('loop_body')
567
+ const exitLabel = this.builder.freshLabel('loop_exit')
568
+
569
+ this.builder.emitJump(checkLabel)
570
+
571
+ // Check block
572
+ this.builder.startBlock(checkLabel)
573
+ const condVar = this.lowerExpr(stmt.cond)
574
+ const condName = this.operandToVar(condVar)
575
+ this.builder.emitJumpIf(condName, bodyLabel, exitLabel)
576
+
577
+ // Body block
578
+ this.builder.startBlock(bodyLabel)
579
+ this.lowerBlock(stmt.body)
580
+ if (!this.builder.isBlockSealed()) {
581
+ this.builder.emitJump(checkLabel)
582
+ }
583
+
584
+ // Exit block
585
+ this.builder.startBlock(exitLabel)
586
+ }
587
+
588
+ private lowerForStmt(stmt: Extract<Stmt, { kind: 'for' }>): void {
589
+ // For loop is lowered to: init; while(cond) { body; step; }
590
+
591
+ // Init statement (if present)
592
+ if (stmt.init) {
593
+ this.lowerStmt(stmt.init)
594
+ }
595
+
596
+ const checkLabel = this.builder.freshLabel('for_check')
597
+ const bodyLabel = this.builder.freshLabel('for_body')
598
+ const exitLabel = this.builder.freshLabel('for_exit')
599
+
600
+ this.builder.emitJump(checkLabel)
601
+
602
+ // Check block
603
+ this.builder.startBlock(checkLabel)
604
+ const condVar = this.lowerExpr(stmt.cond)
605
+ const condName = this.operandToVar(condVar)
606
+ this.builder.emitJumpIf(condName, bodyLabel, exitLabel)
607
+
608
+ // Body block
609
+ this.builder.startBlock(bodyLabel)
610
+ this.lowerBlock(stmt.body)
611
+ // Step expression
612
+ this.lowerExpr(stmt.step)
613
+ if (!this.builder.isBlockSealed()) {
614
+ this.builder.emitJump(checkLabel)
615
+ }
616
+
617
+ // Exit block
618
+ this.builder.startBlock(exitLabel)
619
+ }
620
+
621
+ private lowerForRangeStmt(stmt: Extract<Stmt, { kind: 'for_range' }>): void {
622
+ const loopVar = `$${stmt.varName}`
623
+ const subFnName = `${this.currentFn}/__for_${this.foreachCounter++}`
624
+
625
+ // Initialize loop variable
626
+ this.varMap.set(stmt.varName, loopVar)
627
+ const startVal = this.lowerExpr(stmt.start)
628
+ if (startVal.kind === 'const') {
629
+ this.builder.emitRaw(`scoreboard players set ${loopVar} rs ${startVal.value}`)
630
+ } else if (startVal.kind === 'var') {
631
+ this.builder.emitRaw(`scoreboard players operation ${loopVar} rs = ${startVal.name} rs`)
632
+ }
633
+
634
+ // Call loop function
635
+ this.builder.emitRaw(`function ${this.namespace}:${subFnName}`)
636
+
637
+ // Generate loop sub-function
638
+ const savedBuilder = this.builder
639
+ const savedVarMap = new Map(this.varMap)
640
+ const savedContext = this.currentContext
641
+ const savedBlockPosVars = new Map(this.blockPosVars)
642
+
643
+ this.builder = new LoweringBuilder()
644
+ this.varMap = new Map(savedVarMap)
645
+ this.currentContext = savedContext
646
+ this.blockPosVars = new Map(savedBlockPosVars)
647
+
648
+ this.builder.startBlock('entry')
649
+
650
+ // Body
651
+ this.lowerBlock(stmt.body)
652
+
653
+ // Increment
654
+ this.builder.emitRaw(`scoreboard players add ${loopVar} rs 1`)
655
+
656
+ // Loop condition: execute if score matches ..<end-1> run function
657
+ const endVal = this.lowerExpr(stmt.end)
658
+ const endNum = endVal.kind === 'const' ? endVal.value - 1 : '?'
659
+ this.builder.emitRaw(`execute if score ${loopVar} rs matches ..${endNum} run function ${this.namespace}:${subFnName}`)
660
+
661
+ if (!this.builder.isBlockSealed()) {
662
+ this.builder.emitReturn()
663
+ }
664
+
665
+ const subFn = this.builder.build(subFnName, [], false)
666
+ this.functions.push(subFn)
667
+
668
+ // Restore
669
+ this.builder = savedBuilder
670
+ this.varMap = savedVarMap
671
+ this.currentContext = savedContext
672
+ this.blockPosVars = savedBlockPosVars
673
+ }
674
+
675
+ private lowerForeachStmt(stmt: Extract<Stmt, { kind: 'foreach' }>): void {
676
+ if (stmt.iterable.kind !== 'selector') {
677
+ this.lowerArrayForeachStmt(stmt)
678
+ return
679
+ }
680
+
681
+ // Extract body into a separate function
682
+ const subFnName = `${this.currentFn}/foreach_${this.foreachCounter++}`
683
+ const selector = this.exprToString(stmt.iterable)
684
+
685
+ // Emit execute as ... run function ...
686
+ this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`)
687
+
688
+ // Create the sub-function
689
+ const savedBuilder = this.builder
690
+ const savedVarMap = new Map(this.varMap)
691
+ const savedContext = this.currentContext
692
+ const savedBlockPosVars = new Map(this.blockPosVars)
693
+
694
+ this.builder = new LoweringBuilder()
695
+ this.varMap = new Map(savedVarMap)
696
+ this.currentContext = { binding: stmt.binding }
697
+ this.blockPosVars = new Map(savedBlockPosVars)
698
+
699
+ // In foreach body, the binding maps to @s
700
+ this.varMap.set(stmt.binding, '@s')
701
+
702
+ this.builder.startBlock('entry')
703
+ this.lowerBlock(stmt.body)
704
+ if (!this.builder.isBlockSealed()) {
705
+ this.builder.emitReturn()
706
+ }
707
+
708
+ const subFn = this.builder.build(subFnName, [], false)
709
+ this.functions.push(subFn)
710
+
711
+ // Restore
712
+ this.builder = savedBuilder
713
+ this.varMap = savedVarMap
714
+ this.currentContext = savedContext
715
+ this.blockPosVars = savedBlockPosVars
716
+ }
717
+
718
+ private lowerMatchStmt(stmt: Extract<Stmt, { kind: 'match' }>): void {
719
+ const subject = this.operandToVar(this.lowerExpr(stmt.expr))
720
+ const matchedVar = this.builder.freshTemp()
721
+ this.builder.emitAssign(matchedVar, { kind: 'const', value: 0 })
722
+
723
+ let defaultArm: { pattern: Expr | null; body: Block } | null = null
724
+
725
+ for (const arm of stmt.arms) {
726
+ if (arm.pattern === null) {
727
+ defaultArm = arm
728
+ continue
729
+ }
730
+
731
+ const patternValue = this.lowerExpr(arm.pattern)
732
+ if (patternValue.kind !== 'const') {
733
+ throw new Error('Match patterns must lower to compile-time constants')
734
+ }
735
+
736
+ const subFnName = `${this.currentFn}/match_${this.foreachCounter++}`
737
+ this.builder.emitRaw(`execute if score ${matchedVar} rs matches ..0 if score ${subject} rs matches ${patternValue.value} run function ${this.namespace}:${subFnName}`)
738
+ this.emitMatchArmSubFunction(subFnName, matchedVar, arm.body, true)
739
+ }
740
+
741
+ if (defaultArm) {
742
+ const subFnName = `${this.currentFn}/match_${this.foreachCounter++}`
743
+ this.builder.emitRaw(`execute if score ${matchedVar} rs matches ..0 run function ${this.namespace}:${subFnName}`)
744
+ this.emitMatchArmSubFunction(subFnName, matchedVar, defaultArm.body, false)
745
+ }
746
+ }
747
+
748
+ private emitMatchArmSubFunction(name: string, matchedVar: string, body: Block, setMatched: boolean): void {
749
+ const savedBuilder = this.builder
750
+ const savedVarMap = new Map(this.varMap)
751
+ const savedContext = this.currentContext
752
+ const savedBlockPosVars = new Map(this.blockPosVars)
753
+
754
+ this.builder = new LoweringBuilder()
755
+ this.varMap = new Map(savedVarMap)
756
+ this.currentContext = savedContext
757
+ this.blockPosVars = new Map(savedBlockPosVars)
758
+
759
+ this.builder.startBlock('entry')
760
+ if (setMatched) {
761
+ this.builder.emitRaw(`scoreboard players set ${matchedVar} rs 1`)
762
+ }
763
+ this.lowerBlock(body)
764
+ if (!this.builder.isBlockSealed()) {
765
+ this.builder.emitReturn()
766
+ }
767
+
768
+ this.functions.push(this.builder.build(name, [], false))
769
+
770
+ this.builder = savedBuilder
771
+ this.varMap = savedVarMap
772
+ this.currentContext = savedContext
773
+ this.blockPosVars = savedBlockPosVars
774
+ }
775
+
776
+ private lowerArrayForeachStmt(stmt: Extract<Stmt, { kind: 'foreach' }>): void {
777
+ const arrayName = this.getArrayStorageName(stmt.iterable)
778
+ if (!arrayName) {
779
+ this.builder.emitRaw('# Unsupported foreach iterable')
780
+ return
781
+ }
782
+
783
+ const arrayType = this.inferExprType(stmt.iterable)
784
+ const bindingVar = `$${stmt.binding}`
785
+ const indexVar = this.builder.freshTemp()
786
+ const lengthVar = this.builder.freshTemp()
787
+ const condVar = this.builder.freshTemp()
788
+ const oneVar = this.builder.freshTemp()
789
+
790
+ const savedBinding = this.varMap.get(stmt.binding)
791
+ const savedType = this.varTypes.get(stmt.binding)
792
+
793
+ this.varMap.set(stmt.binding, bindingVar)
794
+ if (arrayType?.kind === 'array') {
795
+ this.varTypes.set(stmt.binding, arrayType.elem)
796
+ }
797
+
798
+ this.builder.emitAssign(indexVar, { kind: 'const', value: 0 })
799
+ this.builder.emitAssign(oneVar, { kind: 'const', value: 1 })
800
+ this.builder.emitRaw(`execute store result score ${lengthVar} rs run data get storage rs:heap ${arrayName}`)
801
+
802
+ const checkLabel = this.builder.freshLabel('foreach_array_check')
803
+ const bodyLabel = this.builder.freshLabel('foreach_array_body')
804
+ const exitLabel = this.builder.freshLabel('foreach_array_exit')
805
+
806
+ this.builder.emitJump(checkLabel)
807
+
808
+ this.builder.startBlock(checkLabel)
809
+ this.builder.emitCmp(condVar, { kind: 'var', name: indexVar }, '<', { kind: 'var', name: lengthVar })
810
+ this.builder.emitJumpIf(condVar, bodyLabel, exitLabel)
811
+
812
+ this.builder.startBlock(bodyLabel)
813
+ const element = this.readArrayElement(arrayName, { kind: 'var', name: indexVar })
814
+ this.builder.emitAssign(bindingVar, element)
815
+ this.lowerBlock(stmt.body)
816
+ if (!this.builder.isBlockSealed()) {
817
+ this.builder.emitRaw(`scoreboard players operation ${indexVar} rs += ${oneVar} rs`)
818
+ this.builder.emitJump(checkLabel)
819
+ }
820
+
821
+ this.builder.startBlock(exitLabel)
822
+
823
+ if (savedBinding) {
824
+ this.varMap.set(stmt.binding, savedBinding)
825
+ } else {
826
+ this.varMap.delete(stmt.binding)
827
+ }
828
+
829
+ if (savedType) {
830
+ this.varTypes.set(stmt.binding, savedType)
831
+ } else {
832
+ this.varTypes.delete(stmt.binding)
833
+ }
834
+ }
835
+
836
+ private lowerAsBlockStmt(stmt: Extract<Stmt, { kind: 'as_block' }>): void {
837
+ const selector = this.selectorToString(stmt.selector)
838
+ const subFnName = `${this.currentFn}/as_${this.foreachCounter++}`
839
+
840
+ this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`)
841
+
842
+ // Create sub-function
843
+ const savedBuilder = this.builder
844
+ const savedVarMap = new Map(this.varMap)
845
+ const savedBlockPosVars = new Map(this.blockPosVars)
846
+
847
+ this.builder = new LoweringBuilder()
848
+ this.varMap = new Map(savedVarMap)
849
+ this.blockPosVars = new Map(savedBlockPosVars)
850
+
851
+ this.builder.startBlock('entry')
852
+ this.lowerBlock(stmt.body)
853
+ if (!this.builder.isBlockSealed()) {
854
+ this.builder.emitReturn()
855
+ }
856
+
857
+ const subFn = this.builder.build(subFnName, [], false)
858
+ this.functions.push(subFn)
859
+
860
+ this.builder = savedBuilder
861
+ this.varMap = savedVarMap
862
+ this.blockPosVars = savedBlockPosVars
863
+ }
864
+
865
+ private lowerAtBlockStmt(stmt: Extract<Stmt, { kind: 'at_block' }>): void {
866
+ const selector = this.selectorToString(stmt.selector)
867
+ const subFnName = `${this.currentFn}/at_${this.foreachCounter++}`
868
+
869
+ this.builder.emitRaw(`execute at ${selector} run function ${this.namespace}:${subFnName}`)
870
+
871
+ // Create sub-function
872
+ const savedBuilder = this.builder
873
+ const savedVarMap = new Map(this.varMap)
874
+ const savedBlockPosVars = new Map(this.blockPosVars)
875
+
876
+ this.builder = new LoweringBuilder()
877
+ this.varMap = new Map(savedVarMap)
878
+ this.blockPosVars = new Map(savedBlockPosVars)
879
+
880
+ this.builder.startBlock('entry')
881
+ this.lowerBlock(stmt.body)
882
+ if (!this.builder.isBlockSealed()) {
883
+ this.builder.emitReturn()
884
+ }
885
+
886
+ const subFn = this.builder.build(subFnName, [], false)
887
+ this.functions.push(subFn)
888
+
889
+ this.builder = savedBuilder
890
+ this.varMap = savedVarMap
891
+ this.blockPosVars = savedBlockPosVars
892
+ }
893
+
894
+ private lowerAsAtStmt(stmt: Extract<Stmt, { kind: 'as_at' }>): void {
895
+ const asSel = this.selectorToString(stmt.as_sel)
896
+ const atSel = this.selectorToString(stmt.at_sel)
897
+ const subFnName = `${this.currentFn}/as_at_${this.foreachCounter++}`
898
+
899
+ this.builder.emitRaw(`execute as ${asSel} at ${atSel} run function ${this.namespace}:${subFnName}`)
900
+
901
+ // Create sub-function
902
+ const savedBuilder = this.builder
903
+ const savedVarMap = new Map(this.varMap)
904
+ const savedBlockPosVars = new Map(this.blockPosVars)
905
+
906
+ this.builder = new LoweringBuilder()
907
+ this.varMap = new Map(savedVarMap)
908
+ this.blockPosVars = new Map(savedBlockPosVars)
909
+
910
+ this.builder.startBlock('entry')
911
+ this.lowerBlock(stmt.body)
912
+ if (!this.builder.isBlockSealed()) {
913
+ this.builder.emitReturn()
914
+ }
915
+
916
+ const subFn = this.builder.build(subFnName, [], false)
917
+ this.functions.push(subFn)
918
+
919
+ this.builder = savedBuilder
920
+ this.varMap = savedVarMap
921
+ this.blockPosVars = savedBlockPosVars
922
+ }
923
+
924
+ private lowerExecuteStmt(stmt: Extract<Stmt, { kind: 'execute' }>): void {
925
+ // Build the execute prefix from subcommands
926
+ const parts: string[] = ['execute']
927
+ for (const sub of stmt.subcommands) {
928
+ switch (sub.kind) {
929
+ case 'as':
930
+ parts.push(`as ${this.selectorToString(sub.selector)}`)
931
+ break
932
+ case 'at':
933
+ parts.push(`at ${this.selectorToString(sub.selector)}`)
934
+ break
935
+ case 'if_entity':
936
+ parts.push(`if entity ${this.selectorToString(sub.selector)}`)
937
+ break
938
+ case 'unless_entity':
939
+ parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
940
+ break
941
+ case 'in':
942
+ parts.push(`in ${sub.dimension}`)
943
+ break
944
+ }
945
+ }
946
+
947
+ const subFnName = `${this.currentFn}/exec_${this.foreachCounter++}`
948
+ this.builder.emitRaw(`${parts.join(' ')} run function ${this.namespace}:${subFnName}`)
949
+
950
+ // Create sub-function for the body
951
+ const savedBuilder = this.builder
952
+ const savedVarMap = new Map(this.varMap)
953
+ const savedBlockPosVars = new Map(this.blockPosVars)
954
+
955
+ this.builder = new LoweringBuilder()
956
+ this.varMap = new Map(savedVarMap)
957
+ this.blockPosVars = new Map(savedBlockPosVars)
958
+
959
+ this.builder.startBlock('entry')
960
+ this.lowerBlock(stmt.body)
961
+ if (!this.builder.isBlockSealed()) {
962
+ this.builder.emitReturn()
963
+ }
964
+
965
+ const subFn = this.builder.build(subFnName, [], false)
966
+ this.functions.push(subFn)
967
+
968
+ this.builder = savedBuilder
969
+ this.varMap = savedVarMap
970
+ this.blockPosVars = savedBlockPosVars
971
+ }
972
+
973
+ // -------------------------------------------------------------------------
974
+ // Expression Lowering
975
+ // -------------------------------------------------------------------------
976
+
977
+ private lowerExpr(expr: Expr): Operand {
978
+ switch (expr.kind) {
979
+ case 'int_lit':
980
+ return { kind: 'const', value: expr.value }
981
+
982
+ case 'float_lit':
983
+ // Float stored as fixed-point × 1000
984
+ return { kind: 'const', value: Math.round(expr.value * 1000) }
985
+
986
+ case 'byte_lit':
987
+ return { kind: 'const', value: expr.value }
988
+
989
+ case 'short_lit':
990
+ return { kind: 'const', value: expr.value }
991
+
992
+ case 'long_lit':
993
+ return { kind: 'const', value: expr.value }
994
+
995
+ case 'double_lit':
996
+ return { kind: 'const', value: Math.round(expr.value * 1000) }
997
+
998
+ case 'bool_lit':
999
+ return { kind: 'const', value: expr.value ? 1 : 0 }
1000
+
1001
+ case 'str_lit':
1002
+ // Strings are handled inline in builtins
1003
+ return { kind: 'const', value: 0 } // Placeholder
1004
+
1005
+ case 'mc_name':
1006
+ // MC names (#health, #red) treated as string constants
1007
+ return { kind: 'const', value: 0 } // Handled inline in exprToString
1008
+
1009
+ case 'str_interp':
1010
+ // Interpolated strings are handled inline in message builtins.
1011
+ return { kind: 'const', value: 0 }
1012
+
1013
+ case 'range_lit':
1014
+ // Ranges are handled in context (selectors, etc.)
1015
+ return { kind: 'const', value: 0 }
1016
+
1017
+ case 'blockpos':
1018
+ return { kind: 'const', value: 0 }
1019
+
1020
+ case 'ident': {
1021
+ const constValue = this.constValues.get(expr.name)
1022
+ if (constValue) {
1023
+ return this.lowerConstLiteral(constValue)
1024
+ }
1025
+ const mapped = this.varMap.get(expr.name)
1026
+ if (mapped) {
1027
+ // Check if it's a selector reference (like @s)
1028
+ if (mapped.startsWith('@')) {
1029
+ return { kind: 'var', name: mapped }
1030
+ }
1031
+ return { kind: 'var', name: mapped }
1032
+ }
1033
+ return { kind: 'var', name: `$${expr.name}` }
1034
+ }
1035
+
1036
+ case 'member':
1037
+ if (expr.obj.kind === 'ident' && this.enumDefs.has(expr.obj.name)) {
1038
+ const variants = this.enumDefs.get(expr.obj.name)!
1039
+ const value = variants.get(expr.field)
1040
+ if (value === undefined) {
1041
+ throw new Error(`Unknown enum variant ${expr.obj.name}.${expr.field}`)
1042
+ }
1043
+ return { kind: 'const', value }
1044
+ }
1045
+ return this.lowerMemberExpr(expr)
1046
+
1047
+ case 'selector':
1048
+ // Selectors are handled inline in builtins
1049
+ return { kind: 'var', name: this.selectorToString(expr.sel) }
1050
+
1051
+ case 'binary':
1052
+ return this.lowerBinaryExpr(expr)
1053
+
1054
+ case 'unary':
1055
+ return this.lowerUnaryExpr(expr)
1056
+
1057
+ case 'assign':
1058
+ return this.lowerAssignExpr(expr)
1059
+
1060
+ case 'call':
1061
+ return this.lowerCallExpr(expr)
1062
+
1063
+ case 'invoke':
1064
+ return this.lowerInvokeExpr(expr)
1065
+
1066
+ case 'member_assign':
1067
+ return this.lowerMemberAssign(expr)
1068
+
1069
+ case 'index':
1070
+ return this.lowerIndexExpr(expr)
1071
+
1072
+ case 'struct_lit':
1073
+ // Struct literals should be handled in let statement
1074
+ return { kind: 'const', value: 0 }
1075
+
1076
+ case 'array_lit':
1077
+ // Array literals should be handled in let statement
1078
+ return { kind: 'const', value: 0 }
1079
+
1080
+ case 'lambda':
1081
+ throw new Error('Lambda expressions must be used in a function context')
1082
+ }
1083
+
1084
+ throw new Error(`Unhandled expression kind: ${(expr as { kind: string }).kind}`)
1085
+ }
1086
+
1087
+ private lowerMemberExpr(expr: Extract<Expr, { kind: 'member' }>): Operand {
1088
+ // Check if this is a struct field access
1089
+ if (expr.obj.kind === 'ident') {
1090
+ const varType = this.varTypes.get(expr.obj.name)
1091
+
1092
+ // Check for world object handle (entity selector)
1093
+ const mapped = this.varMap.get(expr.obj.name)
1094
+ if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
1095
+ // World object field access → scoreboard get
1096
+ const dst = this.builder.freshTemp()
1097
+ this.builder.emitRaw(`scoreboard players operation ${dst} rs = ${mapped} rs`)
1098
+ return { kind: 'var', name: dst }
1099
+ }
1100
+
1101
+ if (varType?.kind === 'struct') {
1102
+ const structName = varType.name.toLowerCase()
1103
+ const path = `rs:heap ${structName}_${expr.obj.name}.${expr.field}`
1104
+ const dst = this.builder.freshTemp()
1105
+ // Read from NBT storage into scoreboard
1106
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${path}`)
1107
+ return { kind: 'var', name: dst }
1108
+ }
1109
+
1110
+ // Array length property
1111
+ if (varType?.kind === 'array' && expr.field === 'len') {
1112
+ const dst = this.builder.freshTemp()
1113
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get storage rs:heap ${expr.obj.name}`)
1114
+ return { kind: 'var', name: dst }
1115
+ }
1116
+ }
1117
+
1118
+ // Default behavior: simple member access
1119
+ return { kind: 'var', name: `$${(expr.obj as any).name}_${expr.field}` }
1120
+ }
1121
+
1122
+ private lowerMemberAssign(expr: Extract<Expr, { kind: 'member_assign' }>): Operand {
1123
+ if (expr.obj.kind === 'ident') {
1124
+ const varType = this.varTypes.get(expr.obj.name)
1125
+
1126
+ // Check for world object handle
1127
+ const mapped = this.varMap.get(expr.obj.name)
1128
+ if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
1129
+ const value = this.lowerExpr(expr.value)
1130
+ if (expr.op === '=') {
1131
+ if (value.kind === 'const') {
1132
+ this.builder.emitRaw(`scoreboard players set ${mapped} rs ${value.value}`)
1133
+ } else if (value.kind === 'var') {
1134
+ this.builder.emitRaw(`scoreboard players operation ${mapped} rs = ${value.name} rs`)
1135
+ }
1136
+ } else {
1137
+ // Compound assignment
1138
+ const binOp = expr.op.slice(0, -1)
1139
+ const opMap: Record<string, string> = { '+': '+=', '-': '-=', '*': '*=', '/': '/=', '%': '%=' }
1140
+ if (value.kind === 'const') {
1141
+ const constTemp = this.builder.freshTemp()
1142
+ this.builder.emitAssign(constTemp, value)
1143
+ this.builder.emitRaw(`scoreboard players operation ${mapped} rs ${opMap[binOp]} ${constTemp} rs`)
1144
+ } else if (value.kind === 'var') {
1145
+ this.builder.emitRaw(`scoreboard players operation ${mapped} rs ${opMap[binOp]} ${value.name} rs`)
1146
+ }
1147
+ }
1148
+ return { kind: 'const', value: 0 }
1149
+ }
1150
+
1151
+ if (varType?.kind === 'struct') {
1152
+ const structName = varType.name.toLowerCase()
1153
+ const path = `rs:heap ${structName}_${expr.obj.name}.${expr.field}`
1154
+ const value = this.lowerExpr(expr.value)
1155
+
1156
+ if (expr.op === '=') {
1157
+ if (value.kind === 'const') {
1158
+ this.builder.emitRaw(`data modify storage ${path} set value ${value.value}`)
1159
+ } else if (value.kind === 'var') {
1160
+ this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${value.name} rs`)
1161
+ }
1162
+ } else {
1163
+ // Compound assignment: read, modify, write back
1164
+ const dst = this.builder.freshTemp()
1165
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${path}`)
1166
+ const binOp = expr.op.slice(0, -1)
1167
+ this.builder.emitBinop(dst, { kind: 'var', name: dst }, binOp as any, value)
1168
+ this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${dst} rs`)
1169
+ }
1170
+ return { kind: 'const', value: 0 }
1171
+ }
1172
+ }
1173
+
1174
+ // Default: simple assignment
1175
+ const varName = `$${(expr.obj as any).name}_${expr.field}`
1176
+ const value = this.lowerExpr(expr.value)
1177
+ this.builder.emitAssign(varName, value)
1178
+ return { kind: 'var', name: varName }
1179
+ }
1180
+
1181
+ private lowerIndexExpr(expr: Extract<Expr, { kind: 'index' }>): Operand {
1182
+ const arrayName = this.getArrayStorageName(expr.obj)
1183
+ if (arrayName) {
1184
+ return this.readArrayElement(arrayName, this.lowerExpr(expr.index))
1185
+ }
1186
+ return { kind: 'const', value: 0 }
1187
+ }
1188
+
1189
+ private lowerBinaryExpr(expr: Extract<Expr, { kind: 'binary' }>): Operand {
1190
+ const left = this.lowerExpr(expr.left)
1191
+ const right = this.lowerExpr(expr.right)
1192
+ const dst = this.builder.freshTemp()
1193
+
1194
+ if (['&&', '||'].includes(expr.op)) {
1195
+ // Logical operators need special handling
1196
+ if (expr.op === '&&') {
1197
+ // Short-circuit AND
1198
+ this.builder.emitAssign(dst, left)
1199
+ const rightVar = this.operandToVar(right)
1200
+ // dst = dst && right → if dst != 0 then dst = right
1201
+ this.builder.emitRaw(`execute if score ${dst} rs matches 1.. run scoreboard players operation ${dst} rs = ${rightVar} rs`)
1202
+ } else {
1203
+ // Short-circuit OR
1204
+ this.builder.emitAssign(dst, left)
1205
+ const rightVar = this.operandToVar(right)
1206
+ // dst = dst || right → if dst == 0 then dst = right
1207
+ this.builder.emitRaw(`execute if score ${dst} rs matches ..0 run scoreboard players operation ${dst} rs = ${rightVar} rs`)
1208
+ }
1209
+ return { kind: 'var', name: dst }
1210
+ }
1211
+
1212
+ if (['==', '!=', '<', '<=', '>', '>='].includes(expr.op)) {
1213
+ this.builder.emitCmp(dst, left, expr.op as CmpOp, right)
1214
+ } else {
1215
+ // Check if this is float arithmetic
1216
+ const isFloatOp = this.isFloatExpr(expr.left) || this.isFloatExpr(expr.right)
1217
+
1218
+ if (isFloatOp && (expr.op === '*' || expr.op === '/')) {
1219
+ // Float multiplication: a * b / 1000
1220
+ // Float division: a * 1000 / b
1221
+ if (expr.op === '*') {
1222
+ this.builder.emitBinop(dst, left, '*', right)
1223
+ // Divide by 1000 to correct for double scaling
1224
+ const constDiv = this.builder.freshTemp()
1225
+ this.builder.emitAssign(constDiv, { kind: 'const', value: 1000 })
1226
+ this.builder.emitRaw(`scoreboard players operation ${dst} rs /= ${constDiv} rs`)
1227
+ } else {
1228
+ // Division: a * 1000 / b
1229
+ const constMul = this.builder.freshTemp()
1230
+ this.builder.emitAssign(constMul, { kind: 'const', value: 1000 })
1231
+ this.builder.emitAssign(dst, left)
1232
+ this.builder.emitRaw(`scoreboard players operation ${dst} rs *= ${constMul} rs`)
1233
+ const rightVar = this.operandToVar(right)
1234
+ this.builder.emitRaw(`scoreboard players operation ${dst} rs /= ${rightVar} rs`)
1235
+ }
1236
+ return { kind: 'var', name: dst }
1237
+ }
1238
+
1239
+ this.builder.emitBinop(dst, left, expr.op as BinOp, right)
1240
+ }
1241
+
1242
+ return { kind: 'var', name: dst }
1243
+ }
1244
+
1245
+ private isFloatExpr(expr: Expr): boolean {
1246
+ if (expr.kind === 'float_lit') return true
1247
+ if (expr.kind === 'ident') {
1248
+ return this.floatVars.has(expr.name)
1249
+ }
1250
+ if (expr.kind === 'binary') {
1251
+ return this.isFloatExpr(expr.left) || this.isFloatExpr(expr.right)
1252
+ }
1253
+ return false
1254
+ }
1255
+
1256
+ private lowerUnaryExpr(expr: Extract<Expr, { kind: 'unary' }>): Operand {
1257
+ const operand = this.lowerExpr(expr.operand)
1258
+ const dst = this.builder.freshTemp()
1259
+
1260
+ if (expr.op === '!') {
1261
+ // Logical NOT: dst = (operand == 0) ? 1 : 0
1262
+ this.builder.emitCmp(dst, operand, '==', { kind: 'const', value: 0 })
1263
+ } else if (expr.op === '-') {
1264
+ // Negation: dst = 0 - operand
1265
+ this.builder.emitBinop(dst, { kind: 'const', value: 0 }, '-', operand)
1266
+ }
1267
+
1268
+ return { kind: 'var', name: dst }
1269
+ }
1270
+
1271
+ private lowerAssignExpr(expr: Extract<Expr, { kind: 'assign' }>): Operand {
1272
+ const blockPosValue = this.resolveBlockPosExpr(expr.value)
1273
+ if (blockPosValue) {
1274
+ this.blockPosVars.set(expr.target, blockPosValue)
1275
+ return { kind: 'const', value: 0 }
1276
+ }
1277
+
1278
+ this.blockPosVars.delete(expr.target)
1279
+ const targetType = this.varTypes.get(expr.target)
1280
+ if (targetType?.kind === 'named' && targetType.name === 'string' && this.storeStringValue(expr.target, expr.value)) {
1281
+ return { kind: 'const', value: 0 }
1282
+ }
1283
+ const varName = this.varMap.get(expr.target) ?? `$${expr.target}`
1284
+ const value = this.lowerExpr(expr.value)
1285
+
1286
+ if (expr.op === '=') {
1287
+ this.builder.emitAssign(varName, value)
1288
+ } else {
1289
+ // Compound assignment
1290
+ const binOp = expr.op.slice(0, -1) as BinOp // Remove '='
1291
+ const dst = this.builder.freshTemp()
1292
+ this.builder.emitBinop(dst, { kind: 'var', name: varName }, binOp, value)
1293
+ this.builder.emitAssign(varName, { kind: 'var', name: dst })
1294
+ }
1295
+
1296
+ return { kind: 'var', name: varName }
1297
+ }
1298
+
1299
+ private lowerCallExpr(expr: Extract<Expr, { kind: 'call' }>): Operand {
1300
+ if (expr.fn === 'str_len') {
1301
+ const storagePath = this.getStringStoragePath(expr.args[0])
1302
+ if (storagePath) {
1303
+ const dst = this.builder.freshTemp()
1304
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get storage ${storagePath}`)
1305
+ return { kind: 'var', name: dst }
1306
+ }
1307
+
1308
+ const staticString = this.resolveStaticString(expr.args[0])
1309
+ if (staticString !== null) {
1310
+ return { kind: 'const', value: Array.from(staticString).length }
1311
+ } else {
1312
+ const dst = this.builder.freshTemp()
1313
+ this.builder.emitAssign(dst, { kind: 'const', value: 0 })
1314
+ return { kind: 'var', name: dst }
1315
+ }
1316
+ }
1317
+
1318
+ // Check for builtin
1319
+ if (expr.fn in BUILTINS) {
1320
+ return this.lowerBuiltinCall(expr.fn, expr.args, getSpan(expr))
1321
+ }
1322
+
1323
+ // Handle entity methods: __entity_tag, __entity_untag, __entity_has_tag
1324
+ if (expr.fn === '__entity_tag') {
1325
+ const entity = this.exprToString(expr.args[0])
1326
+ const tagName = this.exprToString(expr.args[1])
1327
+ this.builder.emitRaw(`tag ${entity} add ${tagName}`)
1328
+ return { kind: 'const', value: 0 }
1329
+ }
1330
+
1331
+ if (expr.fn === '__entity_untag') {
1332
+ const entity = this.exprToString(expr.args[0])
1333
+ const tagName = this.exprToString(expr.args[1])
1334
+ this.builder.emitRaw(`tag ${entity} remove ${tagName}`)
1335
+ return { kind: 'const', value: 0 }
1336
+ }
1337
+
1338
+ if (expr.fn === '__entity_has_tag') {
1339
+ const entity = this.exprToString(expr.args[0])
1340
+ const tagName = this.exprToString(expr.args[1])
1341
+ const dst = this.builder.freshTemp()
1342
+ this.builder.emitRaw(`execute store result score ${dst} rs if entity ${entity}[tag=${tagName}]`)
1343
+ return { kind: 'var', name: dst }
1344
+ }
1345
+
1346
+ // Handle array push
1347
+ if (expr.fn === '__array_push') {
1348
+ const arrExpr = expr.args[0]
1349
+ const valueExpr = expr.args[1]
1350
+ const arrName = this.getArrayStorageName(arrExpr)
1351
+ if (arrName) {
1352
+ const value = this.lowerExpr(valueExpr)
1353
+ if (value.kind === 'const') {
1354
+ this.builder.emitRaw(`data modify storage rs:heap ${arrName} append value ${value.value}`)
1355
+ } else if (value.kind === 'var') {
1356
+ this.builder.emitRaw(`data modify storage rs:heap ${arrName} append value 0`)
1357
+ this.builder.emitRaw(`execute store result storage rs:heap ${arrName}[-1] int 1 run scoreboard players get ${value.name} rs`)
1358
+ }
1359
+ }
1360
+ return { kind: 'const', value: 0 }
1361
+ }
1362
+
1363
+ if (expr.fn === '__array_pop') {
1364
+ const arrName = this.getArrayStorageName(expr.args[0])
1365
+ const dst = this.builder.freshTemp()
1366
+ if (arrName) {
1367
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get storage rs:heap ${arrName}[-1]`)
1368
+ this.builder.emitRaw(`data remove storage rs:heap ${arrName}[-1]`)
1369
+ } else {
1370
+ this.builder.emitAssign(dst, { kind: 'const', value: 0 })
1371
+ }
1372
+ return { kind: 'var', name: dst }
1373
+ }
1374
+
1375
+ // Handle spawn_object - creates world object (invisible armor stand)
1376
+ if (expr.fn === 'spawn_object') {
1377
+ const x = this.exprToString(expr.args[0])
1378
+ const y = this.exprToString(expr.args[1])
1379
+ const z = this.exprToString(expr.args[2])
1380
+ const tag = `__rs_obj_${this.worldObjCounter++}`
1381
+ this.builder.emitRaw(`summon minecraft:armor_stand ${x} ${y} ${z} {Invisible:1b,Marker:1b,NoGravity:1b,Tags:["${tag}"]}`)
1382
+ // Return a selector pointing to this entity
1383
+ const selector = `@e[tag=${tag},limit=1]`
1384
+ return { kind: 'var', name: selector }
1385
+ }
1386
+
1387
+ // Handle kill for world objects
1388
+ if (expr.fn === 'kill' && expr.args.length === 1 && expr.args[0].kind === 'ident') {
1389
+ const mapped = this.varMap.get(expr.args[0].name)
1390
+ if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
1391
+ this.builder.emitRaw(`kill ${mapped}`)
1392
+ return { kind: 'const', value: 0 }
1393
+ }
1394
+ }
1395
+
1396
+ const callbackTarget = this.resolveFunctionRefByName(expr.fn)
1397
+ if (callbackTarget) {
1398
+ return this.emitDirectFunctionCall(callbackTarget, expr.args)
1399
+ }
1400
+
1401
+ // Regular function call
1402
+ const fnDecl = this.fnDecls.get(expr.fn)
1403
+ const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
1404
+ const fullArgs = [...expr.args]
1405
+ for (let i = fullArgs.length; i < defaultArgs.length; i++) {
1406
+ const defaultExpr = defaultArgs[i]
1407
+ if (!defaultExpr) {
1408
+ break
1409
+ }
1410
+ fullArgs.push(defaultExpr)
1411
+ }
1412
+
1413
+ if (fnDecl) {
1414
+ const callbackBindings = new Map<string, string>()
1415
+ const runtimeArgs: Expr[] = []
1416
+
1417
+ for (let i = 0; i < fullArgs.length; i++) {
1418
+ const param = fnDecl.params[i]
1419
+ if (param && this.normalizeType(param.type).kind === 'function_type') {
1420
+ const functionRef = this.resolveFunctionRefExpr(fullArgs[i])
1421
+ if (!functionRef) {
1422
+ throw new Error(`Cannot lower callback argument for parameter '${param.name}'`)
1423
+ }
1424
+ callbackBindings.set(param.name, functionRef)
1425
+ continue
1426
+ }
1427
+ runtimeArgs.push(fullArgs[i])
1428
+ }
1429
+
1430
+ const targetFn = callbackBindings.size > 0
1431
+ ? this.ensureSpecializedFunction(fnDecl, callbackBindings)
1432
+ : expr.fn
1433
+ return this.emitDirectFunctionCall(targetFn, runtimeArgs)
1434
+ }
1435
+
1436
+ return this.emitDirectFunctionCall(expr.fn, fullArgs)
1437
+ }
1438
+
1439
+ private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
1440
+ if (expr.callee.kind === 'lambda') {
1441
+ if (!Array.isArray(expr.callee.body)) {
1442
+ return this.inlineLambdaInvoke(expr.callee, expr.args)
1443
+ }
1444
+ const lambdaName = this.lowerLambdaExpr(expr.callee)
1445
+ return this.emitDirectFunctionCall(lambdaName, expr.args)
1446
+ }
1447
+
1448
+ const functionRef = this.resolveFunctionRefExpr(expr.callee)
1449
+ if (!functionRef) {
1450
+ throw new Error('Cannot invoke a non-function value')
1451
+ }
1452
+ return this.emitDirectFunctionCall(functionRef, expr.args)
1453
+ }
1454
+
1455
+ private inlineLambdaInvoke(expr: Extract<Expr, { kind: 'lambda' }>, args: Expr[]): Operand {
1456
+ const savedVarMap = new Map(this.varMap)
1457
+ const savedVarTypes = new Map(this.varTypes)
1458
+ const savedLambdaBindings = new Map(this.lambdaBindings)
1459
+ const savedBlockPosVars = new Map(this.blockPosVars)
1460
+
1461
+ for (let i = 0; i < expr.params.length; i++) {
1462
+ const param = expr.params[i]
1463
+ const temp = this.builder.freshTemp()
1464
+ const arg = args[i]
1465
+ this.builder.emitAssign(temp, arg ? this.lowerExpr(arg) : { kind: 'const', value: 0 })
1466
+ this.varMap.set(param.name, temp)
1467
+ if (param.type) {
1468
+ this.varTypes.set(param.name, this.normalizeType(param.type))
1469
+ }
1470
+ this.lambdaBindings.delete(param.name)
1471
+ this.blockPosVars.delete(param.name)
1472
+ }
1473
+
1474
+ const result = this.lowerExpr(expr.body as Expr)
1475
+
1476
+ this.varMap = savedVarMap
1477
+ this.varTypes = savedVarTypes
1478
+ this.lambdaBindings = savedLambdaBindings
1479
+ this.blockPosVars = savedBlockPosVars
1480
+ return result
1481
+ }
1482
+
1483
+ private emitDirectFunctionCall(fn: string, args: Expr[]): Operand {
1484
+ const loweredArgs: Operand[] = args.map(arg => this.lowerExpr(arg))
1485
+ const dst = this.builder.freshTemp()
1486
+ this.builder.emitCall(fn, loweredArgs, dst)
1487
+ return { kind: 'var', name: dst }
1488
+ }
1489
+
1490
+ private resolveFunctionRefExpr(expr: Expr): string | null {
1491
+ if (expr.kind === 'lambda') {
1492
+ return this.lowerLambdaExpr(expr)
1493
+ }
1494
+ if (expr.kind === 'ident') {
1495
+ return this.resolveFunctionRefByName(expr.name) ?? (this.fnDecls.has(expr.name) ? expr.name : null)
1496
+ }
1497
+ return null
1498
+ }
1499
+
1500
+ private resolveFunctionRefByName(name: string): string | null {
1501
+ return this.lambdaBindings.get(name) ?? this.currentCallbackBindings.get(name) ?? null
1502
+ }
1503
+
1504
+ private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
1505
+ const parts = [...callbackBindings.entries()]
1506
+ .sort(([left], [right]) => left.localeCompare(right))
1507
+ .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
1508
+ const key = `${fn.name}::${parts.join('::')}`
1509
+ const cached = this.specializedFunctions.get(key)
1510
+ if (cached) {
1511
+ return cached
1512
+ }
1513
+
1514
+ const specializedName = `${fn.name}__${parts.join('__')}`
1515
+ this.specializedFunctions.set(key, specializedName)
1516
+ this.withSavedFunctionState(() => {
1517
+ this.lowerFn(fn, { name: specializedName, callbackBindings })
1518
+ })
1519
+ return specializedName
1520
+ }
1521
+
1522
+ private lowerLambdaExpr(expr: Extract<Expr, { kind: 'lambda' }>): string {
1523
+ const lambdaName = `__lambda_${this.lambdaCounter++}`
1524
+ const lambdaFn: FnDecl = {
1525
+ name: lambdaName,
1526
+ params: expr.params.map(param => ({
1527
+ name: param.name,
1528
+ type: param.type ?? { kind: 'named', name: 'int' },
1529
+ })),
1530
+ returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
1531
+ decorators: [],
1532
+ body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
1533
+ }
1534
+ this.withSavedFunctionState(() => {
1535
+ this.lowerFn(lambdaFn)
1536
+ })
1537
+ return lambdaName
1538
+ }
1539
+
1540
+ private withSavedFunctionState<T>(callback: () => T): T {
1541
+ const savedCurrentFn = this.currentFn
1542
+ const savedForeachCounter = this.foreachCounter
1543
+ const savedBuilder = this.builder
1544
+ const savedVarMap = new Map(this.varMap)
1545
+ const savedLambdaBindings = new Map(this.lambdaBindings)
1546
+ const savedCallbackBindings = new Map(this.currentCallbackBindings)
1547
+ const savedContext = this.currentContext
1548
+ const savedBlockPosVars = new Map(this.blockPosVars)
1549
+ const savedStringValues = new Map(this.stringValues)
1550
+ const savedVarTypes = new Map(this.varTypes)
1551
+
1552
+ try {
1553
+ return callback()
1554
+ } finally {
1555
+ this.currentFn = savedCurrentFn
1556
+ this.foreachCounter = savedForeachCounter
1557
+ this.builder = savedBuilder
1558
+ this.varMap = savedVarMap
1559
+ this.lambdaBindings = savedLambdaBindings
1560
+ this.currentCallbackBindings = savedCallbackBindings
1561
+ this.currentContext = savedContext
1562
+ this.blockPosVars = savedBlockPosVars
1563
+ this.stringValues = savedStringValues
1564
+ this.varTypes = savedVarTypes
1565
+ }
1566
+ }
1567
+
1568
+ private lowerBuiltinCall(name: string, args: Expr[], callSpan?: Span): Operand {
1569
+ const richTextCommand = this.lowerRichTextBuiltin(name, args)
1570
+ if (richTextCommand) {
1571
+ this.builder.emitRaw(richTextCommand)
1572
+ return { kind: 'const', value: 0 }
1573
+ }
1574
+
1575
+ // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
1576
+ if (name === 'random') {
1577
+ const dst = this.builder.freshTemp()
1578
+ const min = args[0] ? this.exprToLiteral(args[0]) : '0'
1579
+ const max = args[1] ? this.exprToLiteral(args[1]) : '100'
1580
+ this.builder.emitRaw(`scoreboard players random ${dst} rs ${min} ${max}`)
1581
+ return { kind: 'var', name: dst }
1582
+ }
1583
+
1584
+ // Special case: random_native - /random value (MC 1.20.3+)
1585
+ if (name === 'random_native') {
1586
+ const dst = this.builder.freshTemp()
1587
+ const min = args[0] ? this.exprToLiteral(args[0]) : '0'
1588
+ const max = args[1] ? this.exprToLiteral(args[1]) : '100'
1589
+ this.builder.emitRaw(`execute store result score ${dst} rs run random value ${min} ${max}`)
1590
+ return { kind: 'var', name: dst }
1591
+ }
1592
+
1593
+ // Special case: random_sequence - /random reset (MC 1.20.3+)
1594
+ if (name === 'random_sequence') {
1595
+ const sequence = this.exprToString(args[0])
1596
+ const seed = args[1] ? this.exprToLiteral(args[1]) : '0'
1597
+ this.builder.emitRaw(`random reset ${sequence} ${seed}`)
1598
+ return { kind: 'const', value: 0 }
1599
+ }
1600
+
1601
+ // Special case: scoreboard_get / score — read from vanilla MC scoreboard
1602
+ if (name === 'scoreboard_get' || name === 'score') {
1603
+ const dst = this.builder.freshTemp()
1604
+ const player = this.exprToTargetString(args[0])
1605
+ const objective = this.exprToString(args[1])
1606
+ this.builder.emitRaw(`execute store result score ${dst} rs run scoreboard players get ${player} ${objective}`)
1607
+ return { kind: 'var', name: dst }
1608
+ }
1609
+
1610
+ // Special case: scoreboard_set — write to vanilla MC scoreboard
1611
+ if (name === 'scoreboard_set') {
1612
+ const player = this.exprToTargetString(args[0])
1613
+ const objective = this.exprToString(args[1])
1614
+ const value = this.lowerExpr(args[2])
1615
+ if (value.kind === 'const') {
1616
+ this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
1617
+ } else if (value.kind === 'var') {
1618
+ // Read directly from the computed scoreboard temp. Routing through a fresh
1619
+ // temp here breaks once optimization removes the apparently-dead assign.
1620
+ this.builder.emitRaw(`execute store result score ${player} ${objective} run scoreboard players get ${value.name} rs`)
1621
+ }
1622
+ return { kind: 'const', value: 0 }
1623
+ }
1624
+
1625
+ if (name === 'scoreboard_display') {
1626
+ const slot = this.exprToString(args[0])
1627
+ const objective = this.exprToString(args[1])
1628
+ this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
1629
+ return { kind: 'const', value: 0 }
1630
+ }
1631
+
1632
+ if (name === 'scoreboard_hide') {
1633
+ const slot = this.exprToString(args[0])
1634
+ this.builder.emitRaw(`scoreboard objectives setdisplay ${slot}`)
1635
+ return { kind: 'const', value: 0 }
1636
+ }
1637
+
1638
+ if (name === 'scoreboard_add_objective') {
1639
+ const objective = this.exprToString(args[0])
1640
+ const criteria = this.exprToString(args[1])
1641
+ const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
1642
+ this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
1643
+ return { kind: 'const', value: 0 }
1644
+ }
1645
+
1646
+ if (name === 'scoreboard_remove_objective') {
1647
+ const objective = this.exprToString(args[0])
1648
+ this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
1649
+ return { kind: 'const', value: 0 }
1650
+ }
1651
+
1652
+ if (name === 'bossbar_add') {
1653
+ const id = this.exprToString(args[0])
1654
+ const title = this.exprToTextComponent(args[1])
1655
+ this.builder.emitRaw(`bossbar add ${id} ${title}`)
1656
+ return { kind: 'const', value: 0 }
1657
+ }
1658
+
1659
+ if (name === 'bossbar_set_value') {
1660
+ this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} value ${this.exprToString(args[1])}`)
1661
+ return { kind: 'const', value: 0 }
1662
+ }
1663
+
1664
+ if (name === 'bossbar_set_max') {
1665
+ this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} max ${this.exprToString(args[1])}`)
1666
+ return { kind: 'const', value: 0 }
1667
+ }
1668
+
1669
+ if (name === 'bossbar_set_color') {
1670
+ this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} color ${this.exprToString(args[1])}`)
1671
+ return { kind: 'const', value: 0 }
1672
+ }
1673
+
1674
+ if (name === 'bossbar_set_style') {
1675
+ this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} style ${this.exprToString(args[1])}`)
1676
+ return { kind: 'const', value: 0 }
1677
+ }
1678
+
1679
+ if (name === 'bossbar_set_visible') {
1680
+ this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} visible ${this.exprToBoolString(args[1])}`)
1681
+ return { kind: 'const', value: 0 }
1682
+ }
1683
+
1684
+ if (name === 'bossbar_set_players') {
1685
+ this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} players ${this.exprToTargetString(args[1])}`)
1686
+ return { kind: 'const', value: 0 }
1687
+ }
1688
+
1689
+ if (name === 'bossbar_remove') {
1690
+ this.builder.emitRaw(`bossbar remove ${this.exprToString(args[0])}`)
1691
+ return { kind: 'const', value: 0 }
1692
+ }
1693
+
1694
+ if (name === 'bossbar_get_value') {
1695
+ const dst = this.builder.freshTemp()
1696
+ this.builder.emitRaw(`execute store result score ${dst} rs run bossbar get ${this.exprToString(args[0])} value`)
1697
+ return { kind: 'var', name: dst }
1698
+ }
1699
+
1700
+ if (name === 'team_add') {
1701
+ const team = this.exprToString(args[0])
1702
+ const displayName = args[1] ? ` ${this.exprToTextComponent(args[1])}` : ''
1703
+ this.builder.emitRaw(`team add ${team}${displayName}`)
1704
+ return { kind: 'const', value: 0 }
1705
+ }
1706
+
1707
+ if (name === 'team_remove') {
1708
+ this.builder.emitRaw(`team remove ${this.exprToString(args[0])}`)
1709
+ return { kind: 'const', value: 0 }
1710
+ }
1711
+
1712
+ if (name === 'team_join') {
1713
+ this.builder.emitRaw(`team join ${this.exprToString(args[0])} ${this.exprToTargetString(args[1])}`)
1714
+ return { kind: 'const', value: 0 }
1715
+ }
1716
+
1717
+ if (name === 'team_leave') {
1718
+ this.builder.emitRaw(`team leave ${this.exprToTargetString(args[0])}`)
1719
+ return { kind: 'const', value: 0 }
1720
+ }
1721
+
1722
+ if (name === 'team_option') {
1723
+ const team = this.exprToString(args[0])
1724
+ const option = this.exprToString(args[1])
1725
+ const value = this.isTeamTextOption(option)
1726
+ ? this.exprToTextComponent(args[2])
1727
+ : this.exprToString(args[2])
1728
+ this.builder.emitRaw(`team modify ${team} ${option} ${value}`)
1729
+ return { kind: 'const', value: 0 }
1730
+ }
1731
+
1732
+ // Special case: data_get — read NBT data into a variable
1733
+ // data_get(target_type, target, path, scale?)
1734
+ // target_type: "entity", "block", "storage"
1735
+ if (name === 'data_get') {
1736
+ const dst = this.builder.freshTemp()
1737
+ const targetType = this.exprToString(args[0])
1738
+ const target = targetType === 'entity'
1739
+ ? this.exprToTargetString(args[1])
1740
+ : this.exprToString(args[1])
1741
+ const path = this.exprToString(args[2])
1742
+ const scale = args[3] ? this.exprToString(args[3]) : '1'
1743
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get ${targetType} ${target} ${path} ${scale}`)
1744
+ return { kind: 'var', name: dst }
1745
+ }
1746
+
1747
+ const coordCommand = this.lowerCoordinateBuiltin(name, args)
1748
+ if (coordCommand) {
1749
+ this.builder.emitRaw(coordCommand)
1750
+ return { kind: 'const', value: 0 }
1751
+ }
1752
+
1753
+ if (name === 'tp_to') {
1754
+ this.warnings.push({
1755
+ message: 'tp_to is deprecated; use tp instead',
1756
+ code: 'W_DEPRECATED',
1757
+ ...(callSpan ? { line: callSpan.line, col: callSpan.col } : {}),
1758
+ })
1759
+ const tpCommand = this.lowerTpCommand(args)
1760
+ if (tpCommand) {
1761
+ this.builder.emitRaw(tpCommand)
1762
+ }
1763
+ return { kind: 'const', value: 0 }
1764
+ }
1765
+
1766
+ if (name === 'tp') {
1767
+ const tpCommand = this.lowerTpCommand(args)
1768
+ if (tpCommand) {
1769
+ this.builder.emitRaw(tpCommand)
1770
+ }
1771
+ return { kind: 'const', value: 0 }
1772
+ }
1773
+
1774
+ // Convert args to strings for builtin (use SNBT for struct/array literals)
1775
+ const strArgs = args.map(arg =>
1776
+ arg.kind === 'struct_lit' || arg.kind === 'array_lit'
1777
+ ? this.exprToSnbt(arg)
1778
+ : this.exprToString(arg)
1779
+ )
1780
+ const cmd = BUILTINS[name](strArgs)
1781
+ if (cmd) {
1782
+ this.builder.emitRaw(cmd)
1783
+ }
1784
+
1785
+ return { kind: 'const', value: 0 }
1786
+ }
1787
+
1788
+ private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
1789
+ const messageArgIndex = this.getRichTextArgIndex(name)
1790
+ if (messageArgIndex === null) {
1791
+ return null
1792
+ }
1793
+
1794
+ const messageExpr = args[messageArgIndex]
1795
+ if (!messageExpr || messageExpr.kind !== 'str_interp') {
1796
+ return null
1797
+ }
1798
+
1799
+ const json = this.buildRichTextJson(messageExpr)
1800
+
1801
+ switch (name) {
1802
+ case 'say':
1803
+ case 'announce':
1804
+ return `tellraw @a ${json}`
1805
+ case 'tell':
1806
+ return `tellraw ${this.exprToString(args[0])} ${json}`
1807
+ case 'title':
1808
+ return `title ${this.exprToString(args[0])} title ${json}`
1809
+ case 'actionbar':
1810
+ return `title ${this.exprToString(args[0])} actionbar ${json}`
1811
+ case 'subtitle':
1812
+ return `title ${this.exprToString(args[0])} subtitle ${json}`
1813
+ default:
1814
+ return null
1815
+ }
1816
+ }
1817
+
1818
+ private getRichTextArgIndex(name: string): number | null {
1819
+ switch (name) {
1820
+ case 'say':
1821
+ case 'announce':
1822
+ return 0
1823
+ case 'tell':
1824
+ case 'title':
1825
+ case 'actionbar':
1826
+ case 'subtitle':
1827
+ return 1
1828
+ default:
1829
+ return null
1830
+ }
1831
+ }
1832
+
1833
+ private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' }>): string {
1834
+ const components: Array<string | Record<string, unknown>> = ['']
1835
+
1836
+ for (const part of expr.parts) {
1837
+ if (typeof part === 'string') {
1838
+ if (part.length > 0) {
1839
+ components.push({ text: part })
1840
+ }
1841
+ continue
1842
+ }
1843
+
1844
+ this.appendRichTextExpr(components, part)
1845
+ }
1846
+
1847
+ return JSON.stringify(components)
1848
+ }
1849
+
1850
+ private appendRichTextExpr(components: Array<string | Record<string, unknown>>, expr: Expr): void {
1851
+ if (expr.kind === 'ident') {
1852
+ const constValue = this.constValues.get(expr.name)
1853
+ if (constValue) {
1854
+ this.appendRichTextExpr(components, constValue)
1855
+ return
1856
+ }
1857
+ const stringValue = this.stringValues.get(expr.name)
1858
+ if (stringValue !== undefined) {
1859
+ components.push({ text: stringValue })
1860
+ return
1861
+ }
1862
+ }
1863
+
1864
+ if (expr.kind === 'str_lit') {
1865
+ if (expr.value.length > 0) {
1866
+ components.push({ text: expr.value })
1867
+ }
1868
+ return
1869
+ }
1870
+
1871
+ if (expr.kind === 'str_interp') {
1872
+ for (const part of expr.parts) {
1873
+ if (typeof part === 'string') {
1874
+ if (part.length > 0) {
1875
+ components.push({ text: part })
1876
+ }
1877
+ } else {
1878
+ this.appendRichTextExpr(components, part)
1879
+ }
1880
+ }
1881
+ return
1882
+ }
1883
+
1884
+ if (expr.kind === 'bool_lit') {
1885
+ components.push({ text: expr.value ? 'true' : 'false' })
1886
+ return
1887
+ }
1888
+
1889
+ if (expr.kind === 'int_lit') {
1890
+ components.push({ text: expr.value.toString() })
1891
+ return
1892
+ }
1893
+
1894
+ if (expr.kind === 'float_lit') {
1895
+ components.push({ text: expr.value.toString() })
1896
+ return
1897
+ }
1898
+
1899
+ const operand = this.lowerExpr(expr)
1900
+ if (operand.kind === 'const') {
1901
+ components.push({ text: operand.value.toString() })
1902
+ return
1903
+ }
1904
+
1905
+ components.push({ score: { name: this.operandToVar(operand), objective: 'rs' } })
1906
+ }
1907
+
1908
+ private exprToString(expr: Expr): string {
1909
+ switch (expr.kind) {
1910
+ case 'int_lit':
1911
+ return expr.value.toString()
1912
+ case 'float_lit':
1913
+ return Math.trunc(expr.value).toString()
1914
+ case 'byte_lit':
1915
+ return `${expr.value}b`
1916
+ case 'short_lit':
1917
+ return `${expr.value}s`
1918
+ case 'long_lit':
1919
+ return `${expr.value}L`
1920
+ case 'double_lit':
1921
+ return `${expr.value}d`
1922
+ case 'bool_lit':
1923
+ return expr.value ? '1' : '0'
1924
+ case 'str_lit':
1925
+ return expr.value
1926
+ case 'mc_name':
1927
+ return expr.value // #health → "health" (no quotes, used as bare MC name)
1928
+ case 'str_interp':
1929
+ return this.buildRichTextJson(expr)
1930
+ case 'blockpos':
1931
+ return emitBlockPos(expr)
1932
+ case 'ident': {
1933
+ const constValue = this.constValues.get(expr.name)
1934
+ if (constValue) {
1935
+ return this.exprToString(constValue)
1936
+ }
1937
+ const stringValue = this.stringValues.get(expr.name)
1938
+ if (stringValue !== undefined) {
1939
+ return stringValue
1940
+ }
1941
+ const mapped = this.varMap.get(expr.name)
1942
+ return mapped ?? `$${expr.name}`
1943
+ }
1944
+ case 'selector':
1945
+ return this.selectorToString(expr.sel)
1946
+ default:
1947
+ // Complex expression - lower and return var name
1948
+ const op = this.lowerExpr(expr)
1949
+ return this.operandToVar(op)
1950
+ }
1951
+ }
1952
+
1953
+ private exprToSnbt(expr: Expr): string {
1954
+ switch (expr.kind) {
1955
+ case 'struct_lit': {
1956
+ const entries = expr.fields.map(f => `${f.name}:${this.exprToSnbt(f.value)}`)
1957
+ return `{${entries.join(',')}}`
1958
+ }
1959
+ case 'array_lit': {
1960
+ const items = expr.elements.map(e => this.exprToSnbt(e))
1961
+ return `[${items.join(',')}]`
1962
+ }
1963
+ case 'str_lit':
1964
+ return `"${expr.value}"`
1965
+ case 'int_lit':
1966
+ return String(expr.value)
1967
+ case 'float_lit':
1968
+ return String(expr.value)
1969
+ case 'byte_lit':
1970
+ return `${expr.value}b`
1971
+ case 'short_lit':
1972
+ return `${expr.value}s`
1973
+ case 'long_lit':
1974
+ return `${expr.value}L`
1975
+ case 'double_lit':
1976
+ return `${expr.value}d`
1977
+ case 'bool_lit':
1978
+ return expr.value ? '1b' : '0b'
1979
+ default:
1980
+ return this.exprToString(expr)
1981
+ }
1982
+ }
1983
+
1984
+ private exprToTargetString(expr: Expr): string {
1985
+ if (expr.kind === 'selector') {
1986
+ return this.selectorToString(expr.sel)
1987
+ }
1988
+
1989
+ if (expr.kind === 'str_lit' && expr.value.startsWith('@')) {
1990
+ const span = getSpan(expr)
1991
+ this.warnings.push({
1992
+ message: `Quoted selector "${expr.value}" is deprecated; pass ${expr.value} without quotes`,
1993
+ code: 'W_QUOTED_SELECTOR',
1994
+ ...(span ? { line: span.line, col: span.col } : {}),
1995
+ })
1996
+ return expr.value
1997
+ }
1998
+
1999
+ return this.exprToString(expr)
2000
+ }
2001
+
2002
+ private exprToLiteral(expr: Expr): string {
2003
+ if (expr.kind === 'int_lit') return expr.value.toString()
2004
+ if (expr.kind === 'float_lit') return Math.trunc(expr.value).toString()
2005
+ return '0'
2006
+ }
2007
+
2008
+ private exprToQuotedString(expr: Expr): string {
2009
+ return JSON.stringify(this.exprToString(expr))
2010
+ }
2011
+
2012
+ private exprToTextComponent(expr: Expr): string {
2013
+ return JSON.stringify({ text: this.exprToString(expr) })
2014
+ }
2015
+
2016
+ private exprToBoolString(expr: Expr): string {
2017
+ if (expr.kind === 'bool_lit') {
2018
+ return expr.value ? 'true' : 'false'
2019
+ }
2020
+ return this.exprToString(expr)
2021
+ }
2022
+
2023
+ private isTeamTextOption(option: string): boolean {
2024
+ return option === 'displayName' || option === 'prefix' || option === 'suffix'
2025
+ }
2026
+
2027
+ private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
2028
+ const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
2029
+ const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
2030
+ const pos2 = args[2] ? this.resolveBlockPosExpr(args[2]) : null
2031
+
2032
+ if (name === 'setblock') {
2033
+ if (args.length === 2 && pos0) {
2034
+ return `setblock ${emitBlockPos(pos0)} ${this.exprToString(args[1])}`
2035
+ }
2036
+ return null
2037
+ }
2038
+
2039
+ if (name === 'fill') {
2040
+ if (args.length === 3 && pos0 && pos1) {
2041
+ return `fill ${emitBlockPos(pos0)} ${emitBlockPos(pos1)} ${this.exprToString(args[2])}`
2042
+ }
2043
+ return null
2044
+ }
2045
+
2046
+ if (name === 'clone') {
2047
+ if (args.length === 3 && pos0 && pos1 && pos2) {
2048
+ return `clone ${emitBlockPos(pos0)} ${emitBlockPos(pos1)} ${emitBlockPos(pos2)}`
2049
+ }
2050
+ return null
2051
+ }
2052
+
2053
+ return null
2054
+ }
2055
+
2056
+ private lowerTpCommand(args: Expr[]): string | null {
2057
+ const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
2058
+ const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
2059
+
2060
+ if (args.length === 1 && pos0) {
2061
+ return `tp ${emitBlockPos(pos0)}`
2062
+ }
2063
+
2064
+ if (args.length === 2) {
2065
+ if (pos1) {
2066
+ return `tp ${this.exprToString(args[0])} ${emitBlockPos(pos1)}`
2067
+ }
2068
+ return `tp ${this.exprToString(args[0])} ${this.exprToString(args[1])}`
2069
+ }
2070
+
2071
+ if (args.length === 4) {
2072
+ return `tp ${this.exprToString(args[0])} ${this.exprToString(args[1])} ${this.exprToString(args[2])} ${this.exprToString(args[3])}`
2073
+ }
2074
+
2075
+ return null
2076
+ }
2077
+
2078
+ private resolveBlockPosExpr(expr: Expr): BlockPosExpr | null {
2079
+ if (expr.kind === 'blockpos') {
2080
+ return expr
2081
+ }
2082
+ if (expr.kind === 'ident') {
2083
+ return this.blockPosVars.get(expr.name) ?? null
2084
+ }
2085
+ return null
2086
+ }
2087
+
2088
+ private getArrayStorageName(expr: Expr): string | null {
2089
+ if (expr.kind === 'ident') {
2090
+ return expr.name
2091
+ }
2092
+ return null
2093
+ }
2094
+
2095
+ private inferLambdaReturnType(expr: Extract<Expr, { kind: 'lambda' }>): TypeNode {
2096
+ if (expr.returnType) {
2097
+ return this.normalizeType(expr.returnType)
2098
+ }
2099
+ if (Array.isArray(expr.body)) {
2100
+ return { kind: 'named', name: 'void' }
2101
+ }
2102
+ return this.inferExprType(expr.body) ?? { kind: 'named', name: 'void' }
2103
+ }
2104
+
2105
+ private inferExprType(expr: Expr): TypeNode | undefined {
2106
+ if (expr.kind === 'int_lit') return { kind: 'named', name: 'int' }
2107
+ if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
2108
+ if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
2109
+ if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
2110
+ if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
2111
+ if (expr.kind === 'ident') {
2112
+ const constValue = this.constValues.get(expr.name)
2113
+ if (constValue) {
2114
+ switch (constValue.kind) {
2115
+ case 'int_lit':
2116
+ return { kind: 'named', name: 'int' }
2117
+ case 'float_lit':
2118
+ return { kind: 'named', name: 'float' }
2119
+ case 'bool_lit':
2120
+ return { kind: 'named', name: 'bool' }
2121
+ case 'str_lit':
2122
+ return { kind: 'named', name: 'string' }
2123
+ }
2124
+ }
2125
+ return this.varTypes.get(expr.name)
2126
+ }
2127
+ if (expr.kind === 'lambda') {
2128
+ return {
2129
+ kind: 'function_type',
2130
+ params: expr.params.map(param => this.normalizeType(param.type ?? { kind: 'named', name: 'int' })),
2131
+ return: this.inferLambdaReturnType(expr),
2132
+ }
2133
+ }
2134
+ if (expr.kind === 'call') {
2135
+ return this.fnDecls.get(this.resolveFunctionRefByName(expr.fn) ?? expr.fn)?.returnType
2136
+ }
2137
+ if (expr.kind === 'invoke') {
2138
+ const calleeType = this.inferExprType(expr.callee)
2139
+ if (calleeType?.kind === 'function_type') {
2140
+ return calleeType.return
2141
+ }
2142
+ }
2143
+ if (expr.kind === 'binary') {
2144
+ if (['==', '!=', '<', '<=', '>', '>=', '&&', '||'].includes(expr.op)) {
2145
+ return { kind: 'named', name: 'bool' }
2146
+ }
2147
+ return this.inferExprType(expr.left)
2148
+ }
2149
+ if (expr.kind === 'unary') {
2150
+ return expr.op === '!' ? { kind: 'named', name: 'bool' } : this.inferExprType(expr.operand)
2151
+ }
2152
+ if (expr.kind === 'array_lit') {
2153
+ return {
2154
+ kind: 'array',
2155
+ elem: expr.elements[0] ? (this.inferExprType(expr.elements[0]) ?? { kind: 'named', name: 'int' }) : { kind: 'named', name: 'int' },
2156
+ }
2157
+ }
2158
+ if (expr.kind === 'member' && expr.obj.kind === 'ident' && this.enumDefs.has(expr.obj.name)) {
2159
+ return { kind: 'enum', name: expr.obj.name }
2160
+ }
2161
+ return undefined
2162
+ }
2163
+
2164
+ private normalizeType(type: TypeNode): TypeNode {
2165
+ if (type.kind === 'array') {
2166
+ return { kind: 'array', elem: this.normalizeType(type.elem) }
2167
+ }
2168
+ if (type.kind === 'function_type') {
2169
+ return {
2170
+ kind: 'function_type',
2171
+ params: type.params.map(param => this.normalizeType(param)),
2172
+ return: this.normalizeType(type.return),
2173
+ }
2174
+ }
2175
+ if ((type.kind === 'struct' || type.kind === 'enum') && this.enumDefs.has(type.name)) {
2176
+ return { kind: 'enum', name: type.name }
2177
+ }
2178
+ return type
2179
+ }
2180
+
2181
+ private readArrayElement(arrayName: string, index: Operand): Operand {
2182
+ const dst = this.builder.freshTemp()
2183
+
2184
+ if (index.kind === 'const') {
2185
+ this.builder.emitRaw(`execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[${index.value}]`)
2186
+ return { kind: 'var', name: dst }
2187
+ }
2188
+
2189
+ const macroKey = `__rs_index_${this.foreachCounter++}`
2190
+ const subFnName = `${this.currentFn}/array_get_${this.foreachCounter++}`
2191
+ const indexVar = index.kind === 'var' ? index.name : this.operandToVar(index)
2192
+ this.builder.emitRaw(`execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} rs`)
2193
+ this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
2194
+ this.emitRawSubFunction(
2195
+ subFnName,
2196
+ `$execute store result score ${dst} rs run data get storage rs:heap ${arrayName}[$(${macroKey})]`
2197
+ )
2198
+ return { kind: 'var', name: dst }
2199
+ }
2200
+
2201
+ private emitRawSubFunction(name: string, ...commands: string[]): void {
2202
+ const builder = new LoweringBuilder()
2203
+ builder.startBlock('entry')
2204
+ for (const cmd of commands) {
2205
+ builder.emitRaw(cmd)
2206
+ }
2207
+ builder.emitReturn()
2208
+ this.functions.push(builder.build(name, [], false))
2209
+ }
2210
+
2211
+ // -------------------------------------------------------------------------
2212
+ // Helpers
2213
+ // -------------------------------------------------------------------------
2214
+
2215
+ private storeStringValue(name: string, expr: Expr): boolean {
2216
+ const value = this.resolveStaticString(expr)
2217
+ if (value === null) {
2218
+ this.stringValues.delete(name)
2219
+ return false
2220
+ }
2221
+ this.stringValues.set(name, value)
2222
+ this.builder.emitRaw(`data modify storage rs:strings ${name} set value ${JSON.stringify(value)}`)
2223
+ return true
2224
+ }
2225
+
2226
+ private resolveStaticString(expr: Expr | undefined): string | null {
2227
+ if (!expr) {
2228
+ return null
2229
+ }
2230
+
2231
+ if (expr.kind === 'str_lit') {
2232
+ return expr.value
2233
+ }
2234
+
2235
+ if (expr.kind === 'ident') {
2236
+ const constValue = this.constValues.get(expr.name)
2237
+ if (constValue?.kind === 'str_lit') {
2238
+ return constValue.value
2239
+ }
2240
+ return this.stringValues.get(expr.name) ?? null
2241
+ }
2242
+
2243
+ return null
2244
+ }
2245
+
2246
+ private getStringStoragePath(expr: Expr | undefined): string | null {
2247
+ if (!expr || expr.kind !== 'ident') {
2248
+ return null
2249
+ }
2250
+
2251
+ if (this.stringValues.has(expr.name)) {
2252
+ return `rs:strings ${expr.name}`
2253
+ }
2254
+
2255
+ return null
2256
+ }
2257
+
2258
+ private lowerConstLiteral(expr: ConstDecl['value']): Operand {
2259
+ switch (expr.kind) {
2260
+ case 'int_lit':
2261
+ return { kind: 'const', value: expr.value }
2262
+ case 'float_lit':
2263
+ return { kind: 'const', value: Math.round(expr.value * 1000) }
2264
+ case 'bool_lit':
2265
+ return { kind: 'const', value: expr.value ? 1 : 0 }
2266
+ case 'str_lit':
2267
+ return { kind: 'const', value: 0 }
2268
+ }
2269
+ }
2270
+
2271
+ private operandToVar(op: Operand): string {
2272
+ if (op.kind === 'var') return op.name
2273
+ // Constant needs to be stored in a temp
2274
+ const dst = this.builder.freshTemp()
2275
+ this.builder.emitAssign(dst, op)
2276
+ return dst
2277
+ }
2278
+
2279
+ private selectorToString(sel: EntitySelector): string {
2280
+ const { kind, filters } = sel
2281
+ if (!filters) return this.finalizeSelector(kind)
2282
+
2283
+ const parts: string[] = []
2284
+ if (filters.type) parts.push(`type=${filters.type}`)
2285
+ if (filters.distance) parts.push(`distance=${this.rangeToString(filters.distance)}`)
2286
+ if (filters.tag) filters.tag.forEach(t => parts.push(`tag=${t}`))
2287
+ if (filters.notTag) filters.notTag.forEach(t => parts.push(`tag=!${t}`))
2288
+ if (filters.limit !== undefined) parts.push(`limit=${filters.limit}`)
2289
+ if (filters.sort) parts.push(`sort=${filters.sort}`)
2290
+ if (filters.scores) {
2291
+ const scoreStr = Object.entries(filters.scores)
2292
+ .map(([k, v]) => `${k}=${this.rangeToString(v)}`).join(',')
2293
+ parts.push(`scores={${scoreStr}}`)
2294
+ }
2295
+ if (filters.nbt) parts.push(`nbt=${filters.nbt}`)
2296
+ if (filters.gamemode) parts.push(`gamemode=${filters.gamemode}`)
2297
+
2298
+ return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind)
2299
+ }
2300
+
2301
+ private finalizeSelector(selector: string): string {
2302
+ return normalizeSelector(selector, this.warnings)
2303
+ }
2304
+
2305
+ private rangeToString(r: RangeExpr): string {
2306
+ if (r.min !== undefined && r.max !== undefined) {
2307
+ if (r.min === r.max) return `${r.min}`
2308
+ return `${r.min}..${r.max}`
2309
+ }
2310
+ if (r.min !== undefined) return `${r.min}..`
2311
+ if (r.max !== undefined) return `..${r.max}`
2312
+ return '..'
2313
+ }
2314
+ }
2315
+
2316
+ // ---------------------------------------------------------------------------
2317
+ // LoweringBuilder - Wrapper around IR construction
2318
+ // ---------------------------------------------------------------------------
2319
+
2320
+ class LoweringBuilder {
2321
+ private tempCount = 0
2322
+ private labelCount = 0
2323
+ private blocks: any[] = []
2324
+ private currentBlock: any = null
2325
+ private locals = new Set<string>()
2326
+
2327
+ freshTemp(): string {
2328
+ const name = `$t${this.tempCount++}`
2329
+ this.locals.add(name)
2330
+ return name
2331
+ }
2332
+
2333
+ freshLabel(hint = 'L'): string {
2334
+ return `${hint}_${this.labelCount++}`
2335
+ }
2336
+
2337
+ startBlock(label: string): void {
2338
+ this.currentBlock = { label, instrs: [], term: null }
2339
+ }
2340
+
2341
+ isBlockSealed(): boolean {
2342
+ return this.currentBlock === null || this.currentBlock.term !== null
2343
+ }
2344
+
2345
+ private sealBlock(term: any): void {
2346
+ if (this.currentBlock) {
2347
+ this.currentBlock.term = term
2348
+ this.blocks.push(this.currentBlock)
2349
+ this.currentBlock = null
2350
+ }
2351
+ }
2352
+
2353
+ emitAssign(dst: string, src: Operand): void {
2354
+ if (!dst.startsWith('$') && !dst.startsWith('@')) {
2355
+ dst = '$' + dst
2356
+ }
2357
+ this.locals.add(dst)
2358
+ this.currentBlock?.instrs.push({ op: 'assign', dst, src })
2359
+ }
2360
+
2361
+ emitBinop(dst: string, lhs: Operand, bop: BinOp, rhs: Operand): void {
2362
+ this.locals.add(dst)
2363
+ this.currentBlock?.instrs.push({ op: 'binop', dst, lhs, bop, rhs })
2364
+ }
2365
+
2366
+ emitCmp(dst: string, lhs: Operand, cop: CmpOp, rhs: Operand): void {
2367
+ this.locals.add(dst)
2368
+ this.currentBlock?.instrs.push({ op: 'cmp', dst, lhs, cop, rhs })
2369
+ }
2370
+
2371
+ emitCall(fn: string, args: Operand[], dst?: string): void {
2372
+ if (dst) this.locals.add(dst)
2373
+ this.currentBlock?.instrs.push({ op: 'call', fn, args, dst })
2374
+ }
2375
+
2376
+ emitRaw(cmd: string): void {
2377
+ this.currentBlock?.instrs.push({ op: 'raw', cmd })
2378
+ }
2379
+
2380
+ emitJump(target: string): void {
2381
+ this.sealBlock({ op: 'jump', target })
2382
+ }
2383
+
2384
+ emitJumpIf(cond: string, then: string, else_: string): void {
2385
+ this.sealBlock({ op: 'jump_if', cond, then, else_ })
2386
+ }
2387
+
2388
+ emitReturn(value?: Operand): void {
2389
+ this.sealBlock({ op: 'return', value })
2390
+ }
2391
+
2392
+ build(name: string, params: string[], isTickLoop = false): IRFunction {
2393
+ // Ensure current block is sealed
2394
+ if (this.currentBlock && !this.currentBlock.term) {
2395
+ this.sealBlock({ op: 'return' })
2396
+ }
2397
+
2398
+ return {
2399
+ name,
2400
+ params,
2401
+ locals: Array.from(this.locals),
2402
+ blocks: this.blocks,
2403
+ isTickLoop,
2404
+ }
2405
+ }
2406
+ }