redscript-mc 1.2.30 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/.claude/commands/build-test.md +10 -0
  2. package/.claude/commands/deploy-demo.md +12 -0
  3. package/.claude/commands/stage-status.md +13 -0
  4. package/.claude/settings.json +12 -0
  5. package/.github/workflows/ci.yml +1 -0
  6. package/CLAUDE.md +231 -0
  7. package/demo.gif +0 -0
  8. package/dist/cli.js +2 -554
  9. package/dist/compile.js +2 -266
  10. package/dist/index.js +2 -159
  11. package/dist/lowering/index.js +5 -3
  12. package/dist/src/__tests__/cli.test.d.ts +1 -0
  13. package/dist/src/__tests__/cli.test.js +104 -0
  14. package/dist/src/__tests__/codegen.test.d.ts +1 -0
  15. package/dist/src/__tests__/codegen.test.js +152 -0
  16. package/dist/src/__tests__/compile-all.test.d.ts +10 -0
  17. package/dist/src/__tests__/compile-all.test.js +108 -0
  18. package/dist/src/__tests__/dce.test.d.ts +1 -0
  19. package/dist/src/__tests__/dce.test.js +102 -0
  20. package/dist/src/__tests__/diagnostics.test.d.ts +4 -0
  21. package/dist/src/__tests__/diagnostics.test.js +177 -0
  22. package/dist/src/__tests__/e2e.test.d.ts +6 -0
  23. package/dist/src/__tests__/e2e.test.js +1789 -0
  24. package/dist/src/__tests__/entity-types.test.d.ts +1 -0
  25. package/dist/src/__tests__/entity-types.test.js +203 -0
  26. package/dist/src/__tests__/formatter.test.d.ts +1 -0
  27. package/dist/src/__tests__/formatter.test.js +40 -0
  28. package/dist/src/__tests__/lexer.test.d.ts +1 -0
  29. package/dist/src/__tests__/lexer.test.js +343 -0
  30. package/dist/src/__tests__/lowering.test.d.ts +1 -0
  31. package/dist/src/__tests__/lowering.test.js +1015 -0
  32. package/dist/src/__tests__/macro.test.d.ts +8 -0
  33. package/dist/src/__tests__/macro.test.js +306 -0
  34. package/dist/src/__tests__/mc-integration.test.d.ts +12 -0
  35. package/dist/src/__tests__/mc-integration.test.js +817 -0
  36. package/dist/src/__tests__/mc-syntax.test.d.ts +1 -0
  37. package/dist/src/__tests__/mc-syntax.test.js +124 -0
  38. package/dist/src/__tests__/nbt.test.d.ts +1 -0
  39. package/dist/src/__tests__/nbt.test.js +82 -0
  40. package/dist/src/__tests__/optimizer-advanced.test.d.ts +1 -0
  41. package/dist/src/__tests__/optimizer-advanced.test.js +124 -0
  42. package/dist/src/__tests__/optimizer.test.d.ts +1 -0
  43. package/dist/src/__tests__/optimizer.test.js +149 -0
  44. package/dist/src/__tests__/parser.test.d.ts +1 -0
  45. package/dist/src/__tests__/parser.test.js +807 -0
  46. package/dist/src/__tests__/repl.test.d.ts +1 -0
  47. package/dist/src/__tests__/repl.test.js +27 -0
  48. package/dist/src/__tests__/runtime.test.d.ts +1 -0
  49. package/dist/src/__tests__/runtime.test.js +289 -0
  50. package/dist/src/__tests__/stdlib-advanced.test.d.ts +4 -0
  51. package/dist/src/__tests__/stdlib-advanced.test.js +374 -0
  52. package/dist/src/__tests__/stdlib-bigint.test.d.ts +7 -0
  53. package/dist/src/__tests__/stdlib-bigint.test.js +426 -0
  54. package/dist/src/__tests__/stdlib-math.test.d.ts +7 -0
  55. package/dist/src/__tests__/stdlib-math.test.js +351 -0
  56. package/dist/src/__tests__/stdlib-vec.test.d.ts +4 -0
  57. package/dist/src/__tests__/stdlib-vec.test.js +263 -0
  58. package/dist/src/__tests__/structure-optimizer.test.d.ts +1 -0
  59. package/dist/src/__tests__/structure-optimizer.test.js +33 -0
  60. package/dist/src/__tests__/typechecker.test.d.ts +1 -0
  61. package/dist/src/__tests__/typechecker.test.js +552 -0
  62. package/dist/src/__tests__/var-allocator.test.d.ts +1 -0
  63. package/dist/src/__tests__/var-allocator.test.js +69 -0
  64. package/dist/src/ast/types.d.ts +515 -0
  65. package/dist/src/ast/types.js +9 -0
  66. package/dist/src/builtins/metadata.d.ts +36 -0
  67. package/dist/src/builtins/metadata.js +1014 -0
  68. package/dist/src/cli.d.ts +11 -0
  69. package/dist/src/cli.js +443 -0
  70. package/dist/src/codegen/cmdblock/index.d.ts +26 -0
  71. package/dist/src/codegen/cmdblock/index.js +45 -0
  72. package/dist/src/codegen/mcfunction/index.d.ts +40 -0
  73. package/dist/src/codegen/mcfunction/index.js +606 -0
  74. package/dist/src/codegen/structure/index.d.ts +24 -0
  75. package/dist/src/codegen/structure/index.js +279 -0
  76. package/dist/src/codegen/var-allocator.d.ts +45 -0
  77. package/dist/src/codegen/var-allocator.js +104 -0
  78. package/dist/src/compile.d.ts +37 -0
  79. package/dist/src/compile.js +165 -0
  80. package/dist/src/diagnostics/index.d.ts +44 -0
  81. package/dist/src/diagnostics/index.js +140 -0
  82. package/dist/src/events/types.d.ts +35 -0
  83. package/dist/src/events/types.js +59 -0
  84. package/dist/src/formatter/index.d.ts +1 -0
  85. package/dist/src/formatter/index.js +26 -0
  86. package/dist/src/index.d.ts +22 -0
  87. package/dist/src/index.js +45 -0
  88. package/dist/src/ir/builder.d.ts +33 -0
  89. package/dist/src/ir/builder.js +99 -0
  90. package/dist/src/ir/types.d.ts +132 -0
  91. package/dist/src/ir/types.js +15 -0
  92. package/dist/src/lexer/index.d.ts +37 -0
  93. package/dist/src/lexer/index.js +569 -0
  94. package/dist/src/lowering/index.d.ts +188 -0
  95. package/dist/src/lowering/index.js +3405 -0
  96. package/dist/src/mc-test/client.d.ts +128 -0
  97. package/dist/src/mc-test/client.js +174 -0
  98. package/dist/src/mc-test/runner.d.ts +28 -0
  99. package/dist/src/mc-test/runner.js +151 -0
  100. package/dist/src/mc-test/setup.d.ts +11 -0
  101. package/dist/src/mc-test/setup.js +98 -0
  102. package/dist/src/mc-validator/index.d.ts +17 -0
  103. package/dist/src/mc-validator/index.js +322 -0
  104. package/dist/src/nbt/index.d.ts +86 -0
  105. package/dist/src/nbt/index.js +250 -0
  106. package/dist/src/optimizer/commands.d.ts +38 -0
  107. package/dist/src/optimizer/commands.js +451 -0
  108. package/dist/src/optimizer/dce.d.ts +34 -0
  109. package/dist/src/optimizer/dce.js +639 -0
  110. package/dist/src/optimizer/passes.d.ts +34 -0
  111. package/dist/src/optimizer/passes.js +243 -0
  112. package/dist/src/optimizer/structure.d.ts +9 -0
  113. package/dist/src/optimizer/structure.js +356 -0
  114. package/dist/src/parser/index.d.ts +93 -0
  115. package/dist/src/parser/index.js +1687 -0
  116. package/dist/src/repl.d.ts +16 -0
  117. package/dist/src/repl.js +165 -0
  118. package/dist/src/runtime/index.d.ts +107 -0
  119. package/dist/src/runtime/index.js +1409 -0
  120. package/dist/src/typechecker/index.d.ts +61 -0
  121. package/dist/src/typechecker/index.js +1034 -0
  122. package/dist/src/types/entity-hierarchy.d.ts +29 -0
  123. package/dist/src/types/entity-hierarchy.js +107 -0
  124. package/dist/src2/__tests__/e2e/basic.test.d.ts +8 -0
  125. package/dist/src2/__tests__/e2e/basic.test.js +140 -0
  126. package/dist/src2/__tests__/e2e/macros.test.d.ts +9 -0
  127. package/dist/src2/__tests__/e2e/macros.test.js +182 -0
  128. package/dist/src2/__tests__/e2e/migrate.test.d.ts +13 -0
  129. package/dist/src2/__tests__/e2e/migrate.test.js +2739 -0
  130. package/dist/src2/__tests__/hir/desugar.test.d.ts +1 -0
  131. package/dist/src2/__tests__/hir/desugar.test.js +234 -0
  132. package/dist/src2/__tests__/lir/lower.test.d.ts +1 -0
  133. package/dist/src2/__tests__/lir/lower.test.js +559 -0
  134. package/dist/src2/__tests__/lir/types.test.d.ts +1 -0
  135. package/dist/src2/__tests__/lir/types.test.js +185 -0
  136. package/dist/src2/__tests__/lir/verify.test.d.ts +1 -0
  137. package/dist/src2/__tests__/lir/verify.test.js +221 -0
  138. package/dist/src2/__tests__/mir/arithmetic.test.d.ts +1 -0
  139. package/dist/src2/__tests__/mir/arithmetic.test.js +130 -0
  140. package/dist/src2/__tests__/mir/control-flow.test.d.ts +1 -0
  141. package/dist/src2/__tests__/mir/control-flow.test.js +205 -0
  142. package/dist/src2/__tests__/mir/verify.test.d.ts +1 -0
  143. package/dist/src2/__tests__/mir/verify.test.js +223 -0
  144. package/dist/src2/__tests__/optimizer/block_merge.test.d.ts +1 -0
  145. package/dist/src2/__tests__/optimizer/block_merge.test.js +78 -0
  146. package/dist/src2/__tests__/optimizer/branch_simplify.test.d.ts +1 -0
  147. package/dist/src2/__tests__/optimizer/branch_simplify.test.js +58 -0
  148. package/dist/src2/__tests__/optimizer/constant_fold.test.d.ts +1 -0
  149. package/dist/src2/__tests__/optimizer/constant_fold.test.js +131 -0
  150. package/dist/src2/__tests__/optimizer/copy_prop.test.d.ts +1 -0
  151. package/dist/src2/__tests__/optimizer/copy_prop.test.js +91 -0
  152. package/dist/src2/__tests__/optimizer/dce.test.d.ts +1 -0
  153. package/dist/src2/__tests__/optimizer/dce.test.js +76 -0
  154. package/dist/src2/__tests__/optimizer/pipeline.test.d.ts +1 -0
  155. package/dist/src2/__tests__/optimizer/pipeline.test.js +102 -0
  156. package/dist/src2/emit/compile.d.ts +19 -0
  157. package/dist/src2/emit/compile.js +80 -0
  158. package/dist/src2/emit/index.d.ts +17 -0
  159. package/dist/src2/emit/index.js +172 -0
  160. package/dist/src2/hir/lower.d.ts +15 -0
  161. package/dist/src2/hir/lower.js +378 -0
  162. package/dist/src2/hir/types.d.ts +373 -0
  163. package/dist/src2/hir/types.js +16 -0
  164. package/dist/src2/lir/lower.d.ts +15 -0
  165. package/dist/src2/lir/lower.js +453 -0
  166. package/dist/src2/lir/types.d.ts +136 -0
  167. package/dist/src2/lir/types.js +11 -0
  168. package/dist/src2/lir/verify.d.ts +14 -0
  169. package/dist/src2/lir/verify.js +113 -0
  170. package/dist/src2/mir/lower.d.ts +9 -0
  171. package/dist/src2/mir/lower.js +1030 -0
  172. package/dist/src2/mir/macro.d.ts +22 -0
  173. package/dist/src2/mir/macro.js +168 -0
  174. package/dist/src2/mir/types.d.ts +183 -0
  175. package/dist/src2/mir/types.js +11 -0
  176. package/dist/src2/mir/verify.d.ts +16 -0
  177. package/dist/src2/mir/verify.js +216 -0
  178. package/dist/src2/optimizer/block_merge.d.ts +12 -0
  179. package/dist/src2/optimizer/block_merge.js +84 -0
  180. package/dist/src2/optimizer/branch_simplify.d.ts +9 -0
  181. package/dist/src2/optimizer/branch_simplify.js +28 -0
  182. package/dist/src2/optimizer/constant_fold.d.ts +10 -0
  183. package/dist/src2/optimizer/constant_fold.js +85 -0
  184. package/dist/src2/optimizer/copy_prop.d.ts +9 -0
  185. package/dist/src2/optimizer/copy_prop.js +113 -0
  186. package/dist/src2/optimizer/dce.d.ts +8 -0
  187. package/dist/src2/optimizer/dce.js +155 -0
  188. package/dist/src2/optimizer/pipeline.d.ts +10 -0
  189. package/dist/src2/optimizer/pipeline.js +42 -0
  190. package/dist/tsconfig.tsbuildinfo +1 -0
  191. package/docs/compiler-pipeline-redesign.md +2243 -0
  192. package/docs/optimization-ideas.md +1076 -0
  193. package/editors/vscode/package-lock.json +3 -3
  194. package/editors/vscode/package.json +1 -1
  195. package/jest.config.js +1 -1
  196. package/package.json +6 -5
  197. package/scripts/postbuild.js +15 -0
  198. package/src/__tests__/cli.test.ts +8 -220
  199. package/src/__tests__/dce.test.ts +11 -56
  200. package/src/__tests__/diagnostics.test.ts +59 -38
  201. package/src/__tests__/mc-integration.test.ts +1 -2
  202. package/src/ast/types.ts +6 -1
  203. package/src/cli.ts +29 -156
  204. package/src/compile.ts +6 -162
  205. package/src/index.ts +14 -178
  206. package/src/mc-test/runner.ts +4 -3
  207. package/src/parser/index.ts +1 -1
  208. package/src/repl.ts +1 -1
  209. package/src/runtime/index.ts +1 -1
  210. package/src2/__tests__/e2e/basic.test.ts +154 -0
  211. package/src2/__tests__/e2e/macros.test.ts +199 -0
  212. package/src2/__tests__/e2e/migrate.test.ts +3008 -0
  213. package/src2/__tests__/hir/desugar.test.ts +263 -0
  214. package/src2/__tests__/lir/lower.test.ts +619 -0
  215. package/src2/__tests__/lir/types.test.ts +207 -0
  216. package/src2/__tests__/lir/verify.test.ts +249 -0
  217. package/src2/__tests__/mir/arithmetic.test.ts +156 -0
  218. package/src2/__tests__/mir/control-flow.test.ts +242 -0
  219. package/src2/__tests__/mir/verify.test.ts +254 -0
  220. package/src2/__tests__/optimizer/block_merge.test.ts +84 -0
  221. package/src2/__tests__/optimizer/branch_simplify.test.ts +64 -0
  222. package/src2/__tests__/optimizer/constant_fold.test.ts +145 -0
  223. package/src2/__tests__/optimizer/copy_prop.test.ts +99 -0
  224. package/src2/__tests__/optimizer/dce.test.ts +83 -0
  225. package/src2/__tests__/optimizer/pipeline.test.ts +116 -0
  226. package/src2/emit/compile.ts +99 -0
  227. package/src2/emit/index.ts +222 -0
  228. package/src2/hir/lower.ts +428 -0
  229. package/src2/hir/types.ts +216 -0
  230. package/src2/lir/lower.ts +556 -0
  231. package/src2/lir/types.ts +109 -0
  232. package/src2/lir/verify.ts +129 -0
  233. package/src2/mir/lower.ts +1160 -0
  234. package/src2/mir/macro.ts +167 -0
  235. package/src2/mir/types.ts +106 -0
  236. package/src2/mir/verify.ts +218 -0
  237. package/src2/optimizer/block_merge.ts +93 -0
  238. package/src2/optimizer/branch_simplify.ts +27 -0
  239. package/src2/optimizer/constant_fold.ts +88 -0
  240. package/src2/optimizer/copy_prop.ts +106 -0
  241. package/src2/optimizer/dce.ts +133 -0
  242. package/src2/optimizer/pipeline.ts +44 -0
  243. package/tsconfig.json +2 -2
  244. package/src/__tests__/codegen.test.ts +0 -161
  245. package/src/__tests__/e2e.test.ts +0 -2039
  246. package/src/__tests__/entity-types.test.ts +0 -236
  247. package/src/__tests__/lowering.test.ts +0 -1185
  248. package/src/__tests__/macro.test.ts +0 -343
  249. package/src/__tests__/nbt.test.ts +0 -58
  250. package/src/__tests__/optimizer-advanced.test.ts +0 -144
  251. package/src/__tests__/optimizer.test.ts +0 -162
  252. package/src/__tests__/runtime.test.ts +0 -305
  253. package/src/__tests__/stdlib-advanced.test.ts +0 -379
  254. package/src/__tests__/stdlib-bigint.test.ts +0 -427
  255. package/src/__tests__/stdlib-math.test.ts +0 -374
  256. package/src/__tests__/stdlib-vec.test.ts +0 -259
  257. package/src/__tests__/structure-optimizer.test.ts +0 -38
  258. package/src/__tests__/var-allocator.test.ts +0 -75
  259. package/src/codegen/cmdblock/index.ts +0 -63
  260. package/src/codegen/mcfunction/index.ts +0 -662
  261. package/src/codegen/structure/index.ts +0 -346
  262. package/src/codegen/var-allocator.ts +0 -104
  263. package/src/ir/builder.ts +0 -116
  264. package/src/ir/types.ts +0 -134
  265. package/src/lowering/index.ts +0 -3876
  266. package/src/optimizer/commands.ts +0 -534
  267. package/src/optimizer/dce.ts +0 -679
  268. package/src/optimizer/passes.ts +0 -250
  269. package/src/optimizer/structure.ts +0 -450
@@ -1,3876 +0,0 @@
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 { SourceRange } from '../compile'
13
- import type {
14
- Block, ConstDecl, Decorator, EntitySelector, Expr, FnDecl, GlobalDecl, Program, RangeExpr, Span, Stmt,
15
- StructDecl, TypeNode, ExecuteSubcommand, BlockPosExpr, CoordComponent, EntityTypeName
16
- } from '../ast/types'
17
- import type { GlobalVar } from '../ir/types'
18
- import * as path from 'path'
19
- import { EVENT_TYPES, getEventParamSpecs, isEventTypeName } from '../events/types'
20
- import { getBaseSelectorType, areCompatibleTypes, getConcreteSubtypes } from '../types/entity-hierarchy'
21
-
22
- // ---------------------------------------------------------------------------
23
- // Macro-aware builtins (MC 1.20.2+)
24
- // These builtins generate commands where parameter variables cannot appear
25
- // as literal values (coordinates, entity types, block types), so they
26
- // require MC macro syntax when called with runtime variables.
27
- // ---------------------------------------------------------------------------
28
-
29
- // All builtins support macro parameters - any arg that's a function param
30
- // will automatically use MC 1.20.2+ macro syntax when needed
31
-
32
- // ---------------------------------------------------------------------------
33
- // Scoreboard Objective
34
- // Set per-compilation via setScoreboardObjective() — defaults to 'rs'.
35
- // ---------------------------------------------------------------------------
36
-
37
- /** Current scoreboard objective. Set once per compile() call. */
38
- export let LOWERING_OBJ = 'rs'
39
- export function setScoreboardObjective(obj: string): void { LOWERING_OBJ = obj }
40
-
41
- // ---------------------------------------------------------------------------
42
- // Builtin Functions
43
- // ---------------------------------------------------------------------------
44
-
45
- const BUILTINS: Record<string, (args: string[]) => string | null> = {
46
- say: ([msg]) => `say ${msg}`,
47
- tell: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
48
- tellraw: ([sel, msg]) => `tellraw ${sel} {"text":"${msg}"}`,
49
- title: ([sel, msg]) => `title ${sel} title {"text":"${msg}"}`,
50
- actionbar: ([sel, msg]) => `title ${sel} actionbar {"text":"${msg}"}`,
51
- subtitle: ([sel, msg]) => `title ${sel} subtitle {"text":"${msg}"}`,
52
- title_times: ([sel, fadeIn, stay, fadeOut]) => `title ${sel} times ${fadeIn} ${stay} ${fadeOut}`,
53
- announce: ([msg]) => `tellraw @a {"text":"${msg}"}`,
54
- give: ([sel, item, count, nbt]) => nbt ? `give ${sel} ${item}${nbt} ${count ?? '1'}` : `give ${sel} ${item} ${count ?? '1'}`,
55
- kill: ([sel]) => `kill ${sel ?? '@s'}`,
56
- effect: ([sel, eff, dur, amp]) => `effect give ${sel} ${eff} ${dur ?? '30'} ${amp ?? '0'}`,
57
- effect_clear: ([sel, eff]) => eff ? `effect clear ${sel} ${eff}` : `effect clear ${sel}`,
58
- summon: ([type, x, y, z, nbt]) => {
59
- const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
60
- return nbt ? `summon ${type} ${pos} ${nbt}` : `summon ${type} ${pos}`
61
- },
62
- particle: ([name, x, y, z, dx, dy, dz, speed, count]) => {
63
- const pos = [x ?? '~', y ?? '~', z ?? '~'].join(' ')
64
- // dx/dy/dz/speed/count are optional; omit trailing undefineds
65
- const extra = [dx, dy, dz, speed, count].filter(v => v !== undefined && v !== null)
66
- return extra.length > 0
67
- ? `particle ${name} ${pos} ${extra.join(' ')}`
68
- : `particle ${name} ${pos}`
69
- },
70
- playsound: ([sound, source, sel, x, y, z, volume, pitch, minVolume]) =>
71
- ['playsound', sound, source, sel, x, y, z, volume, pitch, minVolume].filter(Boolean).join(' '),
72
- tp: () => null, // Special handling
73
- tp_to: () => null, // Special handling (deprecated alias)
74
- clear: ([sel, item]) => `clear ${sel} ${item ?? ''}`.trim(),
75
- weather: ([type]) => `weather ${type}`,
76
- time_set: ([val]) => `time set ${val}`,
77
- time_add: ([val]) => `time add ${val}`,
78
- gamerule: ([rule, val]) => `gamerule ${rule} ${val}`,
79
- tag_add: ([sel, tag]) => `tag ${sel} add ${tag}`,
80
- tag_remove: ([sel, tag]) => `tag ${sel} remove ${tag}`,
81
- kick: ([player, reason]) => `kick ${player} ${reason ?? ''}`.trim(),
82
- setblock: ([x, y, z, block]) => `setblock ${x} ${y} ${z} ${block}`,
83
- fill: ([x1, y1, z1, x2, y2, z2, block]) => `fill ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${block}`,
84
- clone: ([x1, y1, z1, x2, y2, z2, dx, dy, dz]) => `clone ${x1} ${y1} ${z1} ${x2} ${y2} ${z2} ${dx} ${dy} ${dz}`,
85
- difficulty: ([level]) => `difficulty ${level}`,
86
- xp_add: ([sel, amount, type]) => `xp add ${sel} ${amount} ${type ?? 'points'}`,
87
- xp_set: ([sel, amount, type]) => `xp set ${sel} ${amount} ${type ?? 'points'}`,
88
- random: () => null, // Special handling
89
- random_native: () => null, // Special handling
90
- random_sequence: () => null, // Special handling
91
- scoreboard_get: () => null, // Special handling (returns value)
92
- scoreboard_set: () => null, // Special handling
93
- score: () => null, // Special handling (same as scoreboard_get)
94
- scoreboard_display: () => null, // Special handling
95
- scoreboard_hide: () => null, // Special handling
96
- scoreboard_add_objective: () => null, // Special handling
97
- scoreboard_remove_objective: () => null, // Special handling
98
- bossbar_add: () => null, // Special handling
99
- bossbar_set_value: () => null, // Special handling
100
- bossbar_set_max: () => null, // Special handling
101
- bossbar_set_color: () => null, // Special handling
102
- bossbar_set_style: () => null, // Special handling
103
- bossbar_set_visible: () => null, // Special handling
104
- bossbar_set_players: () => null, // Special handling
105
- bossbar_remove: () => null, // Special handling
106
- bossbar_get_value: () => null, // Special handling
107
- team_add: () => null, // Special handling
108
- team_remove: () => null, // Special handling
109
- team_join: () => null, // Special handling
110
- team_leave: () => null, // Special handling
111
- team_option: () => null, // Special handling
112
- data_get: () => null, // Special handling (returns value from NBT)
113
- data_merge: () => null, // Special handling (merge NBT)
114
- set_new: () => null, // Special handling (returns set ID)
115
- set_add: () => null, // Special handling
116
- set_contains: () => null, // Special handling (returns 1/0)
117
- set_remove: () => null, // Special handling
118
- set_clear: () => null, // Special handling
119
- setTimeout: () => null, // Special handling
120
- setInterval: () => null, // Special handling
121
- clearInterval: () => null, // Special handling
122
- storage_get_int: () => null, // Special handling (dynamic NBT array read via macro)
123
- storage_set_array: () => null, // Special handling (write literal NBT array to storage)
124
- storage_set_int: () => null, // Special handling (dynamic NBT array write via macro)
125
- }
126
-
127
- export interface Warning {
128
- message: string
129
- code: string
130
- line?: number
131
- col?: number
132
- }
133
-
134
- interface StdlibCallSiteContext {
135
- filePath?: string
136
- line: number
137
- col: number
138
- }
139
-
140
- function getSpan(node: unknown): Span | undefined {
141
- return (node as { span?: Span } | undefined)?.span
142
- }
143
-
144
- const NAMESPACED_ENTITY_TYPE_RE = /^[a-z0-9_.-]+:[a-z0-9_./-]+$/
145
- const BARE_ENTITY_TYPE_RE = /^[a-z0-9_./-]+$/
146
-
147
- const ENTITY_TO_MC_TYPE: Partial<Record<EntityTypeName, string>> = {
148
- Player: 'minecraft:player',
149
- Zombie: 'minecraft:zombie',
150
- Skeleton: 'minecraft:skeleton',
151
- Creeper: 'minecraft:creeper',
152
- Spider: 'minecraft:spider',
153
- Enderman: 'minecraft:enderman',
154
- Blaze: 'minecraft:blaze',
155
- Witch: 'minecraft:witch',
156
- Slime: 'minecraft:slime',
157
- ZombieVillager: 'minecraft:zombie_villager',
158
- Husk: 'minecraft:husk',
159
- Drowned: 'minecraft:drowned',
160
- Stray: 'minecraft:stray',
161
- WitherSkeleton: 'minecraft:wither_skeleton',
162
- CaveSpider: 'minecraft:cave_spider',
163
- Pig: 'minecraft:pig',
164
- Cow: 'minecraft:cow',
165
- Sheep: 'minecraft:sheep',
166
- Chicken: 'minecraft:chicken',
167
- Villager: 'minecraft:villager',
168
- WanderingTrader: 'minecraft:wandering_trader',
169
- ArmorStand: 'minecraft:armor_stand',
170
- Item: 'minecraft:item',
171
- Arrow: 'minecraft:arrow',
172
- }
173
-
174
- function normalizeSelector(selector: string, warnings: Warning[]): string {
175
- return selector.replace(/type=([^,\]]+)/g, (match, entityType) => {
176
- const trimmed = entityType.trim()
177
-
178
- if (trimmed.includes(':')) {
179
- if (!NAMESPACED_ENTITY_TYPE_RE.test(trimmed)) {
180
- throw new DiagnosticError(
181
- 'LoweringError',
182
- `Invalid entity type format: "${trimmed}" (must be namespace:name)`,
183
- { line: 1, col: 1 }
184
- )
185
- }
186
- return match
187
- }
188
-
189
- if (!BARE_ENTITY_TYPE_RE.test(trimmed)) {
190
- throw new DiagnosticError(
191
- 'LoweringError',
192
- `Invalid entity type format: "${trimmed}" (must be namespace:name or bare_name)`,
193
- { line: 1, col: 1 }
194
- )
195
- }
196
-
197
- warnings.push({
198
- message: `Unnamespaced entity type "${trimmed}", auto-qualifying to "minecraft:${trimmed}"`,
199
- code: 'W_UNNAMESPACED_TYPE',
200
- })
201
- return `type=minecraft:${trimmed}`
202
- })
203
- }
204
-
205
- function emitCoord(component: CoordComponent): string {
206
- switch (component.kind) {
207
- case 'absolute':
208
- return String(component.value)
209
- case 'relative':
210
- return component.offset === 0 ? '~' : `~${component.offset}`
211
- case 'local':
212
- return component.offset === 0 ? '^' : `^${component.offset}`
213
- }
214
- }
215
-
216
- function emitBlockPos(pos: BlockPosExpr): string {
217
- return `${emitCoord(pos.x)} ${emitCoord(pos.y)} ${emitCoord(pos.z)}`
218
- }
219
-
220
- // ---------------------------------------------------------------------------
221
- // Lowering Class
222
- // ---------------------------------------------------------------------------
223
-
224
- export class Lowering {
225
- private namespace: string
226
- private readonly sourceRanges: SourceRange[]
227
- private functions: IRFunction[] = []
228
- private globals: GlobalVar[] = []
229
- private globalNames: Map<string, { mutable: boolean }> = new Map()
230
- private fnDecls: Map<string, FnDecl> = new Map()
231
- private implMethods: Map<string, Map<string, { fn: FnDecl; loweredName: string }>> = new Map()
232
- private specializedFunctions: Map<string, string> = new Map()
233
- private currentFn: string = ''
234
-
235
- /** Unique IR variable name for a local variable, scoped to the current function.
236
- * Prevents cross-function scoreboard slot collisions: $fn_x ≠ $gn_x.
237
- * Only applies to user-defined locals/params; internal slots ($p0, $ret) are
238
- * intentionally global (calling convention). */
239
- private fnVar(name: string): string {
240
- return `$${this.currentFn}_${name}`
241
- }
242
- private currentStdlibCallSite?: StdlibCallSiteContext
243
- private foreachCounter: number = 0
244
- private lambdaCounter: number = 0
245
- private timeoutCounter: number = 0
246
- private intervalCounter: number = 0
247
- readonly warnings: Warning[] = []
248
-
249
- // Entity type context stack for W_IMPOSSIBLE_AS warnings
250
- private entityContextStack: string[] = []
251
-
252
- private currentEntityContext(): string {
253
- return this.entityContextStack.length > 0
254
- ? this.entityContextStack[this.entityContextStack.length - 1]
255
- : 'Entity'
256
- }
257
-
258
- // Builder state for current function
259
- private builder!: LoweringBuilder
260
- private varMap: Map<string, string> = new Map()
261
- private lambdaBindings: Map<string, string> = new Map()
262
- private intervalBindings: Map<string, string> = new Map()
263
- private intervalFunctions: Map<number, string> = new Map()
264
- private currentCallbackBindings: Map<string, string> = new Map()
265
- private currentContext: { binding?: string } = {}
266
- private blockPosVars: Map<string, BlockPosExpr> = new Map()
267
-
268
- // Struct definitions: name → { fieldName: TypeNode }
269
- private structDefs: Map<string, Map<string, TypeNode>> = new Map()
270
- // Full struct declarations for field iteration
271
- private structDecls: Map<string, StructDecl> = new Map()
272
- private enumDefs: Map<string, Map<string, number>> = new Map()
273
- private functionDefaults: Map<string, Array<Expr | undefined>> = new Map()
274
- private constValues: Map<string, ConstDecl['value']> = new Map()
275
- private stringValues: Map<string, string> = new Map()
276
- // Variable types: varName → TypeNode
277
- private varTypes: Map<string, TypeNode> = new Map()
278
- // Float variables (stored as fixed-point × 1000)
279
- private floatVars: Set<string> = new Set()
280
- // World object counter for unique tags
281
- private worldObjCounter: number = 0
282
-
283
- // Loop context stack for break/continue
284
- private loopStack: Array<{ breakLabel: string; continueLabel: string; stepFn?: () => void }> = []
285
-
286
- // MC 1.20.2+ macro function support
287
- // Names of params in the current function being lowered
288
- private currentFnParamNames: Set<string> = new Set()
289
- // Params in the current function that need macro treatment (used in literal positions)
290
- private currentFnMacroParams: Set<string> = new Set()
291
- // Global registry: fnName → macroParamNames (populated by pre-scan + lowering)
292
- private macroFunctionInfo: Map<string, string[]> = new Map()
293
-
294
- constructor(namespace: string, sourceRanges: SourceRange[] = []) {
295
- this.namespace = namespace
296
- this.sourceRanges = sourceRanges
297
- LoweringBuilder.resetTempCounter()
298
- }
299
-
300
- // ---------------------------------------------------------------------------
301
- // MC Macro pre-scan: identify which function params need macro treatment
302
- // ---------------------------------------------------------------------------
303
-
304
- private preScanMacroFunctions(program: Program): void {
305
- for (const fn of program.declarations) {
306
- const paramNames = new Set(fn.params.map(p => p.name))
307
- const macroParams = new Set<string>()
308
- this.preScanStmts(fn.body, paramNames, macroParams)
309
- if (macroParams.size > 0) {
310
- this.macroFunctionInfo.set(fn.name, [...macroParams])
311
- }
312
- }
313
- for (const implBlock of program.implBlocks ?? []) {
314
- for (const method of implBlock.methods) {
315
- const paramNames = new Set(method.params.map(p => p.name))
316
- const macroParams = new Set<string>()
317
- this.preScanStmts(method.body, paramNames, macroParams)
318
- if (macroParams.size > 0) {
319
- this.macroFunctionInfo.set(`${implBlock.typeName}_${method.name}`, [...macroParams])
320
- }
321
- }
322
- }
323
- }
324
-
325
- private preScanStmts(stmts: Block, paramNames: Set<string>, macroParams: Set<string>): void {
326
- for (const stmt of stmts) {
327
- this.preScanStmt(stmt, paramNames, macroParams)
328
- }
329
- }
330
-
331
- private preScanStmt(stmt: Stmt, paramNames: Set<string>, macroParams: Set<string>): void {
332
- switch (stmt.kind) {
333
- case 'expr':
334
- this.preScanExpr(stmt.expr, paramNames, macroParams)
335
- break
336
- case 'let':
337
- this.preScanExpr(stmt.init, paramNames, macroParams)
338
- break
339
- case 'return':
340
- if (stmt.value) this.preScanExpr(stmt.value, paramNames, macroParams)
341
- break
342
- case 'if':
343
- this.preScanExpr(stmt.cond, paramNames, macroParams)
344
- this.preScanStmts(stmt.then, paramNames, macroParams)
345
- if (stmt.else_) this.preScanStmts(stmt.else_, paramNames, macroParams)
346
- break
347
- case 'while':
348
- this.preScanExpr(stmt.cond, paramNames, macroParams)
349
- this.preScanStmts(stmt.body, paramNames, macroParams)
350
- break
351
- case 'for':
352
- if (stmt.init) this.preScanStmt(stmt.init, paramNames, macroParams)
353
- this.preScanExpr(stmt.cond, paramNames, macroParams)
354
- this.preScanStmts(stmt.body, paramNames, macroParams)
355
- break
356
- case 'for_range':
357
- this.preScanStmts(stmt.body, paramNames, macroParams)
358
- break
359
- case 'foreach':
360
- this.preScanStmts(stmt.body, paramNames, macroParams)
361
- break
362
- case 'match':
363
- this.preScanExpr(stmt.expr, paramNames, macroParams)
364
- for (const arm of stmt.arms) {
365
- this.preScanStmts(arm.body, paramNames, macroParams)
366
- }
367
- break
368
- case 'as_block':
369
- case 'at_block':
370
- this.preScanStmts(stmt.body, paramNames, macroParams)
371
- break
372
- case 'execute':
373
- this.preScanStmts(stmt.body, paramNames, macroParams)
374
- break
375
- // raw, break, continue have no nested exprs of interest
376
- }
377
- }
378
-
379
- private preScanExpr(expr: Expr, paramNames: Set<string>, macroParams: Set<string>): void {
380
- if (expr.kind === 'call' && BUILTINS[expr.fn] !== undefined) {
381
- // Only trigger macro param detection for builtins that actually emit
382
- // MC commands with $(param) inline in the current function body.
383
- // Special-handled builtins (storage_get_int / storage_set_int / etc.) are
384
- // declared as `() => null` — they create their own sub-functions for macro
385
- // indirection and do NOT require the surrounding function to be a macro.
386
- const handler = BUILTINS[expr.fn]!
387
- const isSpecialHandled: boolean = (() => {
388
- try { return (handler as () => string | null)() === null } catch { return false }
389
- })()
390
- if (!isSpecialHandled) {
391
- for (const arg of expr.args) {
392
- if (arg.kind === 'ident' && paramNames.has(arg.name)) {
393
- macroParams.add(arg.name)
394
- }
395
- }
396
- }
397
- // Always recurse into args for nested calls/expressions
398
- for (const arg of expr.args) this.preScanExpr(arg, paramNames, macroParams)
399
- return
400
- }
401
- // Recurse into sub-expressions for other call types
402
- if (expr.kind === 'call') {
403
- for (const arg of expr.args) this.preScanExpr(arg, paramNames, macroParams)
404
- } else if (expr.kind === 'binary') {
405
- this.preScanExpr(expr.left, paramNames, macroParams)
406
- this.preScanExpr(expr.right, paramNames, macroParams)
407
- } else if (expr.kind === 'unary') {
408
- this.preScanExpr(expr.operand, paramNames, macroParams)
409
- } else if (expr.kind === 'assign') {
410
- this.preScanExpr(expr.value, paramNames, macroParams)
411
- }
412
- }
413
-
414
- // ---------------------------------------------------------------------------
415
- // Macro helpers
416
- // ---------------------------------------------------------------------------
417
-
418
- /**
419
- * If `expr` is a function parameter that needs macro treatment (runtime value
420
- * used in a literal position), returns the param name; otherwise null.
421
- */
422
- private tryGetMacroParam(expr: Expr): string | null {
423
- if (expr.kind !== 'ident') return null
424
- if (!this.currentFnParamNames.has(expr.name)) return null
425
- if (this.constValues.has(expr.name)) return null
426
- if (this.stringValues.has(expr.name)) return null
427
- return expr.name
428
- }
429
-
430
- private tryGetMacroParamByName(name: string): string | null {
431
- if (!this.currentFnParamNames.has(name)) return null
432
- if (this.constValues.has(name)) return null
433
- if (this.stringValues.has(name)) return null
434
- return name
435
- }
436
-
437
- /**
438
- * Converts an expression to a string for use as a builtin arg.
439
- * If the expression is a macro param, returns `$(name)` and sets macroParam.
440
- */
441
- private exprToBuiltinArg(expr: Expr): { str: string; macroParam?: string } {
442
- const macroParam = this.tryGetMacroParam(expr)
443
- if (macroParam) {
444
- return { str: `$(${macroParam})`, macroParam }
445
- }
446
- // Handle ~ident / ^ident syntax — relative/local coord with a VARIABLE offset.
447
- //
448
- // WHY macros are required here:
449
- // Minecraft's ~N and ^N coordinate syntax requires N to be a compile-time
450
- // literal number. There is no command that accepts a scoreboard value as a
451
- // relative offset. Therefore `~height` (where height is a runtime int) can
452
- // only be expressed at the MC level via the 1.20.2+ function macro system,
453
- // which substitutes $(height) into the command text at call time.
454
- //
455
- // Contrast with absolute coords: `tp(target, x, y, z)` where x/y/z are
456
- // plain ints — those become $(x) etc. as literal replacements, same mechanism,
457
- // but the distinction matters to callers: ~$(height) means "relative by height
458
- // blocks from current pos", not "teleport to absolute scoreboard value".
459
- //
460
- // Example:
461
- // fn launch_up(target: selector, height: int) {
462
- // tp(target, ~0, ~height, ~0); // "~height" parsed as rel_coord
463
- // }
464
- // Emits: $tp $(target) ~0 ~$(height) ~0
465
- // Called: function ns:launch_up with storage rs:macro_args
466
- if (expr.kind === 'rel_coord' || expr.kind === 'local_coord') {
467
- const val = expr.value // e.g. "~height" or "^depth"
468
- const prefix = val[0] // ~ or ^
469
- const rest = val.slice(1)
470
- // If rest is an identifier (not a number), treat as macro param
471
- if (rest && /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(rest)) {
472
- const paramName = this.tryGetMacroParamByName(rest)
473
- if (paramName) {
474
- return { str: `${prefix}$(${paramName})`, macroParam: paramName }
475
- }
476
- }
477
- }
478
- if (expr.kind === 'struct_lit' || expr.kind === 'array_lit') {
479
- return { str: this.exprToSnbt(expr) }
480
- }
481
- // Float literals: preserve the float value for MC commands that accept floats
482
- // (particle spread, playsound volume/pitch, etc.)
483
- // We do NOT truncate here — that only applies to scoreboard/IR contexts.
484
- if (expr.kind === 'float_lit') {
485
- return { str: expr.value.toString() }
486
- }
487
- // Unary minus applied to a float literal (e.g. -0.5)
488
- if (expr.kind === 'unary' && expr.op === '-' && expr.operand.kind === 'float_lit') {
489
- return { str: (-expr.operand.value).toString() }
490
- }
491
- return { str: this.exprToString(expr) }
492
- }
493
-
494
- /**
495
- * Emits a call to a macro function, setting up both scoreboard params
496
- * (for arithmetic use) and NBT macro args (for coordinate/literal use).
497
- */
498
- private emitMacroFunctionCall(
499
- fnName: string,
500
- args: Expr[],
501
- macroParamNames: string[],
502
- fnDecl: FnDecl | undefined,
503
- ): Operand {
504
- const params = fnDecl?.params ?? []
505
- const loweredArgs: Operand[] = args.map(arg => this.lowerExpr(arg))
506
-
507
- // Set up regular scoreboard params (for arithmetic within the function)
508
- for (let i = 0; i < loweredArgs.length; i++) {
509
- const operand = loweredArgs[i]
510
- if (operand.kind === 'const') {
511
- this.builder.emitRaw(`scoreboard players set $p${i} ${LOWERING_OBJ} ${operand.value}`)
512
- } else if (operand.kind === 'var') {
513
- this.builder.emitRaw(`scoreboard players operation $p${i} ${LOWERING_OBJ} = ${operand.name} ${LOWERING_OBJ}`)
514
- }
515
- }
516
-
517
- // Set up NBT storage for each macro param
518
- // float-typed params are stored as `double 0.01` so that an integer value N
519
- // becomes N/100.0 in the command (e.g. scoreboard value 975 → NBT 9.75d → ^9.75)
520
- for (const macroParam of macroParamNames) {
521
- const paramIdx = params.findIndex(p => p.name === macroParam)
522
- if (paramIdx < 0 || paramIdx >= loweredArgs.length) continue
523
-
524
- const operand = loweredArgs[paramIdx]
525
- const paramType = params[paramIdx]?.type
526
- const isFloat = paramType?.kind === 'named' && (paramType as any).name === 'float'
527
-
528
- if (operand.kind === 'const') {
529
- if (isFloat) {
530
- const floatVal = (operand.value / 100).toFixed(6)
531
- this.builder.emitRaw(`data modify storage rs:macro_args ${macroParam} set value ${floatVal}d`)
532
- } else {
533
- this.builder.emitRaw(`data modify storage rs:macro_args ${macroParam} set value ${operand.value}`)
534
- }
535
- } else if (operand.kind === 'var') {
536
- if (isFloat) {
537
- this.builder.emitRaw(
538
- `execute store result storage rs:macro_args ${macroParam} double 0.01 run scoreboard players get ${operand.name} ${LOWERING_OBJ}`
539
- )
540
- } else {
541
- this.builder.emitRaw(
542
- `execute store result storage rs:macro_args ${macroParam} int 1 run scoreboard players get ${operand.name} ${LOWERING_OBJ}`
543
- )
544
- }
545
- }
546
- }
547
-
548
- // Call with macro storage
549
- this.builder.emitRaw(`function ${this.namespace}:${fnName} with storage rs:macro_args`)
550
-
551
- // Copy return value (callers may use it)
552
- const dst = this.builder.freshTemp()
553
- this.builder.emitRaw(`scoreboard players operation ${dst} ${LOWERING_OBJ} = $ret ${LOWERING_OBJ}`)
554
- return { kind: 'var', name: dst }
555
- }
556
-
557
- lower(program: Program): IRModule {
558
- this.namespace = program.namespace
559
-
560
- // Pre-scan for macro functions before main lowering (so call sites can detect them)
561
- this.preScanMacroFunctions(program)
562
-
563
- // Load struct definitions
564
- for (const struct of program.structs ?? []) {
565
- const fields = new Map<string, TypeNode>()
566
- for (const field of struct.fields) {
567
- fields.set(field.name, field.type)
568
- }
569
- this.structDefs.set(struct.name, fields)
570
- this.structDecls.set(struct.name, struct)
571
- }
572
-
573
- for (const enumDecl of program.enums ?? []) {
574
- const variants = new Map<string, number>()
575
- for (const variant of enumDecl.variants) {
576
- variants.set(variant.name, variant.value ?? 0)
577
- }
578
- this.enumDefs.set(enumDecl.name, variants)
579
- }
580
-
581
- for (const constDecl of program.consts ?? []) {
582
- this.constValues.set(constDecl.name, constDecl.value)
583
- this.varTypes.set(constDecl.name, this.normalizeType(constDecl.type))
584
- }
585
-
586
- // Process global variable declarations (top-level let)
587
- for (const g of program.globals ?? []) {
588
- this.globalNames.set(g.name, { mutable: g.mutable })
589
- this.varTypes.set(g.name, this.normalizeType(g.type))
590
- const initValue = g.init.kind === 'int_lit' ? g.init.value : 0
591
- this.globals.push({ name: `$${g.name}`, init: initValue })
592
- }
593
-
594
- for (const fn of program.declarations) {
595
- this.fnDecls.set(fn.name, fn)
596
- this.functionDefaults.set(fn.name, fn.params.map(param => param.default))
597
- }
598
-
599
- for (const implBlock of program.implBlocks ?? []) {
600
- let methods = this.implMethods.get(implBlock.typeName)
601
- if (!methods) {
602
- methods = new Map()
603
- this.implMethods.set(implBlock.typeName, methods)
604
- }
605
-
606
- for (const method of implBlock.methods) {
607
- const loweredName = `${implBlock.typeName}_${method.name}`
608
- methods.set(method.name, { fn: method, loweredName })
609
- this.fnDecls.set(loweredName, method)
610
- this.functionDefaults.set(loweredName, method.params.map(param => param.default))
611
- }
612
- }
613
-
614
- for (const fn of program.declarations) {
615
- this.lowerFn(fn)
616
- }
617
-
618
- for (const implBlock of program.implBlocks ?? []) {
619
- for (const method of implBlock.methods) {
620
- this.lowerFn(method, { name: `${implBlock.typeName}_${method.name}` })
621
- }
622
- }
623
-
624
- return buildModule(this.namespace, this.functions, this.globals)
625
- }
626
-
627
- // -------------------------------------------------------------------------
628
- // Function Lowering
629
- // -------------------------------------------------------------------------
630
-
631
- private lowerFn(
632
- fn: FnDecl,
633
- options: {
634
- name?: string
635
- callbackBindings?: Map<string, string>
636
- stdlibCallSite?: StdlibCallSiteContext
637
- } = {}
638
- ): void {
639
- const loweredName = options.name ?? fn.name
640
- const callbackBindings = options.callbackBindings ?? new Map<string, string>()
641
- const stdlibCallSite = options.stdlibCallSite
642
- const staticEventDec = fn.decorators.find(d => d.name === 'on')
643
- const eventType = staticEventDec?.args?.eventType
644
- const eventParamSpecs = eventType && isEventTypeName(eventType) ? getEventParamSpecs(eventType) : []
645
- const runtimeParams = staticEventDec
646
- ? []
647
- : fn.params.filter(param => !callbackBindings.has(param.name))
648
-
649
- this.currentFn = loweredName
650
- this.currentStdlibCallSite = stdlibCallSite
651
- this.foreachCounter = 0
652
- this.varMap = new Map()
653
- this.lambdaBindings = new Map()
654
- this.intervalBindings = new Map()
655
- this.currentCallbackBindings = new Map(callbackBindings)
656
- this.currentContext = {}
657
- this.blockPosVars = new Map()
658
- this.stringValues = new Map()
659
- this.builder = new LoweringBuilder()
660
- // Initialize macro tracking for this function
661
- this.currentFnParamNames = new Set(runtimeParams.map(p => p.name))
662
- this.currentFnMacroParams = new Set()
663
-
664
- // Map parameters
665
- if (staticEventDec) {
666
- for (let i = 0; i < fn.params.length; i++) {
667
- const param = fn.params[i]
668
- const expected = eventParamSpecs[i]
669
- const normalizedType = this.normalizeType(param.type)
670
- this.varTypes.set(param.name, normalizedType)
671
-
672
- if (expected?.type.kind === 'entity') {
673
- this.varMap.set(param.name, '@s')
674
- continue
675
- }
676
-
677
- if (expected?.type.kind === 'named' && expected.type.name === 'string') {
678
- this.stringValues.set(param.name, '')
679
- continue
680
- }
681
-
682
- this.varMap.set(param.name, this.fnVar(param.name))
683
- }
684
- } else {
685
- for (const param of runtimeParams) {
686
- const paramName = param.name
687
- this.varMap.set(paramName, this.fnVar(paramName))
688
- this.varTypes.set(paramName, this.normalizeType(param.type))
689
- }
690
- }
691
- for (const param of fn.params) {
692
- if (callbackBindings.has(param.name)) {
693
- this.varTypes.set(param.name, this.normalizeType(param.type))
694
- }
695
- }
696
-
697
- // Start entry block
698
- this.builder.startBlock('entry')
699
-
700
- // Copy params from the parameter-passing slots to named local variables.
701
- // Use { kind: 'param', index: i } so the codegen resolves to
702
- // alloc.internal('p{i}') consistently in both mangle and no-mangle modes,
703
- // avoiding the slot-collision between the internal register and a user variable
704
- // named 'p0'/'p1' that occurred with { kind: 'var', name: '$p0' }.
705
- for (let i = 0; i < runtimeParams.length; i++) {
706
- const paramName = runtimeParams[i].name
707
- const varName = this.fnVar(paramName)
708
- this.builder.emitAssign(varName, { kind: 'param', index: i })
709
- }
710
-
711
- if (staticEventDec) {
712
- for (let i = 0; i < fn.params.length; i++) {
713
- const param = fn.params[i]
714
- const expected = eventParamSpecs[i]
715
- if (expected?.type.kind === 'named' && expected.type.name !== 'string') {
716
- this.builder.emitAssign(this.fnVar(param.name), { kind: 'const', value: 0 })
717
- }
718
- }
719
- }
720
-
721
- // Lower body
722
- this.lowerBlock(fn.body)
723
-
724
- // If no explicit return, add void return
725
- if (!this.builder.isBlockSealed()) {
726
- this.builder.emitReturn()
727
- }
728
-
729
- // Build function
730
- const isTickLoop = fn.decorators.some(d => d.name === 'tick')
731
- const tickRate = this.getTickRate(fn.decorators)
732
-
733
- // Check for trigger handler
734
- const triggerDec = fn.decorators.find(d => d.name === 'on_trigger')
735
- const isTriggerHandler = !!triggerDec
736
- const triggerName = triggerDec?.args?.trigger
737
-
738
- const irFn = this.builder.build(loweredName, runtimeParams.map(p => `$${p.name}`), isTickLoop)
739
-
740
- // Add trigger metadata if applicable
741
- if (isTriggerHandler && triggerName) {
742
- irFn.isTriggerHandler = true
743
- irFn.triggerName = triggerName
744
- }
745
-
746
- const eventDec = fn.decorators.find(d =>
747
- d.name === 'on_advancement' ||
748
- d.name === 'on_craft' ||
749
- d.name === 'on_death' ||
750
- d.name === 'on_login' ||
751
- d.name === 'on_join_team'
752
- )
753
- if (eventDec) {
754
- switch (eventDec.name) {
755
- case 'on_advancement':
756
- irFn.eventTrigger = { kind: 'advancement', value: eventDec.args?.advancement }
757
- break
758
- case 'on_craft':
759
- irFn.eventTrigger = { kind: 'craft', value: eventDec.args?.item }
760
- break
761
- case 'on_death':
762
- irFn.eventTrigger = { kind: 'death' }
763
- break
764
- case 'on_login':
765
- irFn.eventTrigger = { kind: 'login' }
766
- break
767
- case 'on_join_team':
768
- irFn.eventTrigger = { kind: 'join_team', value: eventDec.args?.team }
769
- break
770
- }
771
- }
772
-
773
- if (eventType && isEventTypeName(eventType)) {
774
- irFn.eventHandler = {
775
- eventType,
776
- tag: EVENT_TYPES[eventType].tag,
777
- }
778
- }
779
-
780
- // Check for @load decorator
781
- if (fn.decorators.some(d => d.name === 'load')) {
782
- irFn.isLoadInit = true
783
- }
784
-
785
- // @requires("dep_fn") — when this function is compiled in, dep_fn is also
786
- // called from __load. The dep_fn itself does NOT need @load; it can be a
787
- // private (_) function that only runs at load time when this fn is used.
788
- const requiredLoads: string[] = []
789
- for (const d of fn.decorators) {
790
- if (d.name === 'require_on_load') {
791
- for (const arg of d.rawArgs ?? []) {
792
- if (arg.kind === 'string') {
793
- requiredLoads.push(arg.value)
794
- }
795
- }
796
- }
797
- }
798
- if (requiredLoads.length > 0) {
799
- irFn.requiredLoads = requiredLoads
800
- }
801
-
802
- // Handle tick rate counter if needed
803
- if (tickRate && tickRate > 1) {
804
- this.wrapWithTickRate(irFn, tickRate)
805
- }
806
-
807
- // Set macro metadata if this function uses MC macro syntax
808
- if (this.currentFnMacroParams.size > 0) {
809
- irFn.isMacroFunction = true
810
- irFn.macroParamNames = [...this.currentFnMacroParams]
811
- // Update registry (may refine the pre-scan result)
812
- this.macroFunctionInfo.set(loweredName, irFn.macroParamNames)
813
- }
814
-
815
- this.functions.push(irFn)
816
- }
817
-
818
- private getTickRate(decorators: Decorator[]): number | undefined {
819
- const tickDec = decorators.find(d => d.name === 'tick')
820
- return tickDec?.args?.rate
821
- }
822
-
823
- private wrapWithTickRate(fn: IRFunction, rate: number): void {
824
- // Add tick counter logic to entry block
825
- const counterVar = `$__tick_${fn.name}`
826
- this.globals.push({ name: counterVar, init: 0 })
827
-
828
- // Prepend counter logic to entry block
829
- const entry = fn.blocks[0]
830
- const originalInstrs = [...entry.instrs]
831
- const originalTerm = entry.term
832
-
833
- entry.instrs = [
834
- { op: 'raw', cmd: `scoreboard players add ${counterVar} ${LOWERING_OBJ} 1` },
835
- ]
836
-
837
- // Create conditional jump
838
- const bodyLabel = 'tick_body'
839
- const skipLabel = 'tick_skip'
840
-
841
- entry.term = {
842
- op: 'jump_if',
843
- cond: `${counterVar}_check`,
844
- then: bodyLabel,
845
- else_: skipLabel,
846
- }
847
-
848
- // Add check instruction
849
- entry.instrs.push({
850
- op: 'raw',
851
- cmd: `execute store success score ${counterVar}_check ${LOWERING_OBJ} if score ${counterVar} ${LOWERING_OBJ} matches ${rate}..`,
852
- })
853
-
854
- // Body block (original logic + counter reset)
855
- fn.blocks.push({
856
- label: bodyLabel,
857
- instrs: [
858
- { op: 'raw', cmd: `scoreboard players set ${counterVar} ${LOWERING_OBJ} 0` },
859
- ...originalInstrs,
860
- ],
861
- term: originalTerm,
862
- })
863
-
864
- // Skip block (just return)
865
- fn.blocks.push({
866
- label: skipLabel,
867
- instrs: [],
868
- term: { op: 'return' },
869
- })
870
- }
871
-
872
- // -------------------------------------------------------------------------
873
- // Statement Lowering
874
- // -------------------------------------------------------------------------
875
-
876
- private lowerBlock(stmts: Block): void {
877
- for (const stmt of stmts) {
878
- this.lowerStmt(stmt)
879
- }
880
- }
881
-
882
- private lowerStmt(stmt: Stmt): void {
883
- switch (stmt.kind) {
884
- case 'let':
885
- this.lowerLetStmt(stmt)
886
- break
887
- case 'expr':
888
- this.lowerExpr(stmt.expr)
889
- break
890
- case 'return':
891
- this.lowerReturnStmt(stmt)
892
- break
893
- case 'break':
894
- this.lowerBreakStmt()
895
- break
896
- case 'continue':
897
- this.lowerContinueStmt()
898
- break
899
- case 'if':
900
- this.lowerIfStmt(stmt)
901
- break
902
- case 'while':
903
- this.lowerWhileStmt(stmt)
904
- break
905
- case 'for':
906
- this.lowerForStmt(stmt)
907
- break
908
- case 'foreach':
909
- this.lowerForeachStmt(stmt)
910
- break
911
- case 'for_range':
912
- this.lowerForRangeStmt(stmt)
913
- break
914
- case 'match':
915
- this.lowerMatchStmt(stmt)
916
- break
917
- case 'as_block':
918
- this.lowerAsBlockStmt(stmt)
919
- break
920
- case 'at_block':
921
- this.lowerAtBlockStmt(stmt)
922
- break
923
- case 'as_at':
924
- this.lowerAsAtStmt(stmt)
925
- break
926
- case 'execute':
927
- this.lowerExecuteStmt(stmt)
928
- break
929
- case 'raw':
930
- this.checkRawCommandInterpolation(stmt.cmd, stmt.span)
931
- this.builder.emitRaw(stmt.cmd)
932
- break
933
- }
934
- }
935
-
936
- private lowerLetStmt(stmt: Extract<Stmt, { kind: 'let' }>): void {
937
- // Check for duplicate declaration of foreach binding
938
- if (this.currentContext.binding === stmt.name) {
939
- throw new DiagnosticError(
940
- 'LoweringError',
941
- `Cannot redeclare foreach binding '${stmt.name}'`,
942
- stmt.span ?? { line: 0, col: 0 }
943
- )
944
- }
945
-
946
- const varName = this.fnVar(stmt.name)
947
- this.varMap.set(stmt.name, varName)
948
-
949
- // Track variable type
950
- const declaredType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init)
951
- if (declaredType) {
952
- this.varTypes.set(stmt.name, declaredType)
953
- // Track float variables for fixed-point arithmetic
954
- if (declaredType.kind === 'named' && declaredType.name === 'float') {
955
- this.floatVars.add(stmt.name)
956
- }
957
- }
958
-
959
- if (stmt.init.kind === 'lambda') {
960
- const lambdaName = this.lowerLambdaExpr(stmt.init)
961
- this.lambdaBindings.set(stmt.name, lambdaName)
962
- return
963
- }
964
-
965
- if (stmt.init.kind === 'call' && stmt.init.fn === 'setInterval') {
966
- const value = this.lowerExpr(stmt.init)
967
- const intervalFn = this.intervalFunctions.get(value.kind === 'const' ? value.value : NaN)
968
- if (intervalFn) {
969
- this.intervalBindings.set(stmt.name, intervalFn)
970
- }
971
- this.builder.emitAssign(varName, value)
972
- return
973
- }
974
-
975
- // Handle struct literal initialization
976
- if (stmt.init.kind === 'struct_lit' && stmt.type?.kind === 'struct') {
977
- const structName = stmt.type.name.toLowerCase()
978
- for (const field of stmt.init.fields) {
979
- const path = `rs:heap ${structName}_${stmt.name}.${field.name}`
980
- const fieldValue = this.lowerExpr(field.value)
981
- if (fieldValue.kind === 'const') {
982
- this.builder.emitRaw(`data modify storage ${path} set value ${fieldValue.value}`)
983
- } else if (fieldValue.kind === 'var') {
984
- // Copy from scoreboard to NBT
985
- this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${fieldValue.name} ${LOWERING_OBJ}`)
986
- }
987
- }
988
- return
989
- }
990
-
991
- // Handle struct initialization from function call (copy from __ret_struct)
992
- if ((stmt.init.kind === 'call' || stmt.init.kind === 'static_call') && stmt.type?.kind === 'struct') {
993
- // First, execute the function call
994
- this.lowerExpr(stmt.init)
995
- // Then copy all fields from __ret_struct to the variable's storage
996
- const structDecl = this.structDecls.get(stmt.type.name)
997
- if (structDecl) {
998
- const structName = stmt.type.name.toLowerCase()
999
- for (const field of structDecl.fields) {
1000
- const srcPath = `rs:heap __ret_struct.${field.name}`
1001
- const dstPath = `rs:heap ${structName}_${stmt.name}.${field.name}`
1002
- this.builder.emitRaw(`data modify storage ${dstPath} set from storage ${srcPath}`)
1003
- }
1004
- }
1005
- return
1006
- }
1007
-
1008
- // Handle array literal initialization
1009
- if (stmt.init.kind === 'array_lit') {
1010
- // Initialize empty NBT array
1011
- this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} set value []`)
1012
- // Add each element
1013
- for (const elem of stmt.init.elements) {
1014
- const elemValue = this.lowerExpr(elem)
1015
- if (elemValue.kind === 'const') {
1016
- this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} append value ${elemValue.value}`)
1017
- } else if (elemValue.kind === 'var') {
1018
- this.builder.emitRaw(`data modify storage rs:heap ${stmt.name} append value 0`)
1019
- this.builder.emitRaw(`execute store result storage rs:heap ${stmt.name}[-1] int 1 run scoreboard players get ${elemValue.name} ${LOWERING_OBJ}`)
1020
- }
1021
- }
1022
- return
1023
- }
1024
-
1025
- // Handle set_new returning a set ID string
1026
- if (stmt.init.kind === 'call' && stmt.init.fn === 'set_new') {
1027
- const setId = `__set_${this.foreachCounter++}`
1028
- this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
1029
- this.stringValues.set(stmt.name, setId)
1030
- return
1031
- }
1032
-
1033
- // Handle spawn_object returning entity handle
1034
- if (stmt.init.kind === 'call' && stmt.init.fn === 'spawn_object') {
1035
- const value = this.lowerExpr(stmt.init)
1036
- // value is the selector like @e[tag=__rs_obj_0,limit=1]
1037
- if (value.kind === 'var' && value.name.startsWith('@e[tag=__rs_obj_')) {
1038
- this.varMap.set(stmt.name, value.name)
1039
- // Mark as entity type for later member access
1040
- this.varTypes.set(stmt.name, { kind: 'named', name: 'void' }) // Marker
1041
- }
1042
- return
1043
- }
1044
-
1045
- const blockPosValue = this.resolveBlockPosExpr(stmt.init)
1046
- if (blockPosValue) {
1047
- this.blockPosVars.set(stmt.name, blockPosValue)
1048
- return
1049
- }
1050
-
1051
- const stmtType = stmt.type ? this.normalizeType(stmt.type) : this.inferExprType(stmt.init)
1052
- if (stmtType?.kind === 'named' && stmtType.name === 'string' && this.storeStringValue(stmt.name, stmt.init)) {
1053
- return
1054
- }
1055
-
1056
- const value = this.lowerExpr(stmt.init)
1057
- this.builder.emitAssign(varName, value)
1058
- }
1059
-
1060
- private lowerReturnStmt(stmt: Extract<Stmt, { kind: 'return' }>): void {
1061
- if (stmt.value) {
1062
- // Handle struct literal return: store fields to __ret_struct storage
1063
- if (stmt.value.kind === 'struct_lit') {
1064
- for (const field of stmt.value.fields) {
1065
- const path = `rs:heap __ret_struct.${field.name}`
1066
- const fieldValue = this.lowerExpr(field.value)
1067
- if (fieldValue.kind === 'const') {
1068
- this.builder.emitRaw(`data modify storage ${path} set value ${fieldValue.value}`)
1069
- } else if (fieldValue.kind === 'var') {
1070
- this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${fieldValue.name} ${LOWERING_OBJ}`)
1071
- }
1072
- }
1073
- this.builder.emitReturn({ kind: 'const', value: 0 })
1074
- return
1075
- }
1076
- const value = this.lowerExpr(stmt.value)
1077
- this.builder.emitReturn(value)
1078
- } else {
1079
- this.builder.emitReturn()
1080
- }
1081
- }
1082
-
1083
- private lowerBreakStmt(): void {
1084
- if (this.loopStack.length === 0) {
1085
- throw new DiagnosticError('LoweringError', 'break statement outside of loop', { line: 1, col: 1 })
1086
- }
1087
- const loop = this.loopStack[this.loopStack.length - 1]
1088
- this.builder.emitJump(loop.breakLabel)
1089
- }
1090
-
1091
- private lowerContinueStmt(): void {
1092
- if (this.loopStack.length === 0) {
1093
- throw new DiagnosticError('LoweringError', 'continue statement outside of loop', { line: 1, col: 1 })
1094
- }
1095
- const loop = this.loopStack[this.loopStack.length - 1]
1096
- this.builder.emitJump(loop.continueLabel)
1097
- }
1098
-
1099
- private lowerIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
1100
- if (stmt.cond.kind === 'is_check') {
1101
- this.lowerIsCheckIfStmt(stmt)
1102
- return
1103
- }
1104
-
1105
- const condVar = this.lowerExpr(stmt.cond)
1106
- const condName = this.operandToVar(condVar)
1107
-
1108
- const thenLabel = this.builder.freshLabel('then')
1109
- const elseLabel = this.builder.freshLabel('else')
1110
- const mergeLabel = this.builder.freshLabel('merge')
1111
-
1112
- this.builder.emitJumpIf(condName, thenLabel, stmt.else_ ? elseLabel : mergeLabel)
1113
-
1114
- // Then block
1115
- this.builder.startBlock(thenLabel)
1116
- this.lowerBlock(stmt.then)
1117
- if (!this.builder.isBlockSealed()) {
1118
- this.builder.emitJump(mergeLabel)
1119
- }
1120
-
1121
- // Else block (if present)
1122
- if (stmt.else_) {
1123
- this.builder.startBlock(elseLabel)
1124
- this.lowerBlock(stmt.else_)
1125
- if (!this.builder.isBlockSealed()) {
1126
- this.builder.emitJump(mergeLabel)
1127
- }
1128
- }
1129
-
1130
- // Merge block
1131
- this.builder.startBlock(mergeLabel)
1132
- }
1133
-
1134
- private lowerIsCheckIfStmt(stmt: Extract<Stmt, { kind: 'if' }>): void {
1135
- const cond = stmt.cond
1136
- if (cond.kind !== 'is_check') {
1137
- throw new DiagnosticError(
1138
- 'LoweringError',
1139
- "Internal error: expected 'is' check condition",
1140
- stmt.span ?? { line: 0, col: 0 }
1141
- )
1142
- }
1143
-
1144
- if (stmt.else_) {
1145
- throw new DiagnosticError(
1146
- 'LoweringError',
1147
- "'is' checks with else branches are not yet supported",
1148
- cond.span ?? stmt.span ?? { line: 0, col: 0 }
1149
- )
1150
- }
1151
-
1152
- const selector = this.exprToEntitySelector(cond.expr)
1153
- if (!selector) {
1154
- throw new DiagnosticError(
1155
- 'LoweringError',
1156
- "'is' checks require an entity selector or entity binding",
1157
- cond.span ?? stmt.span ?? { line: 0, col: 0 }
1158
- )
1159
- }
1160
-
1161
- const mcType = ENTITY_TO_MC_TYPE[cond.entityType]
1162
- const thenFnName = `${this.currentFn}/then_${this.foreachCounter++}`
1163
-
1164
- if (!mcType) {
1165
- // Abstract type — check all concrete subtypes
1166
- const subtypes = getConcreteSubtypes(cond.entityType)
1167
- if (subtypes.length === 0) {
1168
- throw new DiagnosticError(
1169
- 'LoweringError',
1170
- `Cannot lower entity type check for '${cond.entityType}'`,
1171
- cond.span ?? stmt.span ?? { line: 0, col: 0 }
1172
- )
1173
- }
1174
- // Use a temp scoreboard variable to OR multiple type checks
1175
- this.builder.emitRaw(`scoreboard players set __is_result rs:temp 0`)
1176
- for (const subtype of subtypes) {
1177
- if (subtype.mcId) {
1178
- this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, subtype.mcId)} run scoreboard players set __is_result rs:temp 1`)
1179
- }
1180
- }
1181
- this.builder.emitRaw(`execute if score __is_result rs:temp matches 1 run function ${this.namespace}:${thenFnName}`)
1182
- } else {
1183
- // Concrete type — single check
1184
- this.builder.emitRaw(`execute if entity ${this.appendTypeFilter(selector, mcType)} run function ${this.namespace}:${thenFnName}`)
1185
- }
1186
-
1187
- const savedBuilder = this.builder
1188
- const savedVarMap = new Map(this.varMap)
1189
- const savedBlockPosVars = new Map(this.blockPosVars)
1190
-
1191
- this.builder = new LoweringBuilder()
1192
- this.varMap = new Map(savedVarMap)
1193
- this.blockPosVars = new Map(savedBlockPosVars)
1194
-
1195
- this.builder.startBlock('entry')
1196
- this.lowerBlock(stmt.then)
1197
- if (!this.builder.isBlockSealed()) {
1198
- this.builder.emitReturn()
1199
- }
1200
-
1201
- this.functions.push(this.builder.build(thenFnName, [], false))
1202
-
1203
- this.builder = savedBuilder
1204
- this.varMap = savedVarMap
1205
- this.blockPosVars = savedBlockPosVars
1206
- }
1207
-
1208
- private lowerWhileStmt(stmt: Extract<Stmt, { kind: 'while' }>): void {
1209
- const checkLabel = this.builder.freshLabel('loop_check')
1210
- const bodyLabel = this.builder.freshLabel('loop_body')
1211
- const exitLabel = this.builder.freshLabel('loop_exit')
1212
-
1213
- this.builder.emitJump(checkLabel)
1214
-
1215
- // Check block
1216
- this.builder.startBlock(checkLabel)
1217
- const condVar = this.lowerExpr(stmt.cond)
1218
- const condName = this.operandToVar(condVar)
1219
- this.builder.emitJumpIf(condName, bodyLabel, exitLabel)
1220
-
1221
- // Push loop context for break/continue (while has no step, so continue goes to check)
1222
- this.loopStack.push({ breakLabel: exitLabel, continueLabel: checkLabel })
1223
-
1224
- // Body block
1225
- this.builder.startBlock(bodyLabel)
1226
- this.lowerBlock(stmt.body)
1227
- if (!this.builder.isBlockSealed()) {
1228
- this.builder.emitJump(checkLabel)
1229
- }
1230
-
1231
- // Pop loop context
1232
- this.loopStack.pop()
1233
-
1234
- // Exit block
1235
- this.builder.startBlock(exitLabel)
1236
- }
1237
-
1238
- private lowerForStmt(stmt: Extract<Stmt, { kind: 'for' }>): void {
1239
- // For loop is lowered to: init; while(cond) { body; step; }
1240
-
1241
- // Init statement (if present)
1242
- if (stmt.init) {
1243
- this.lowerStmt(stmt.init)
1244
- }
1245
-
1246
- const checkLabel = this.builder.freshLabel('for_check')
1247
- const bodyLabel = this.builder.freshLabel('for_body')
1248
- const continueLabel = this.builder.freshLabel('for_continue')
1249
- const exitLabel = this.builder.freshLabel('for_exit')
1250
-
1251
- this.builder.emitJump(checkLabel)
1252
-
1253
- // Check block
1254
- this.builder.startBlock(checkLabel)
1255
- const condVar = this.lowerExpr(stmt.cond)
1256
- const condName = this.operandToVar(condVar)
1257
- this.builder.emitJumpIf(condName, bodyLabel, exitLabel)
1258
-
1259
- // Push loop context for break/continue
1260
- this.loopStack.push({ breakLabel: exitLabel, continueLabel })
1261
-
1262
- // Body block
1263
- this.builder.startBlock(bodyLabel)
1264
- this.lowerBlock(stmt.body)
1265
- if (!this.builder.isBlockSealed()) {
1266
- this.builder.emitJump(continueLabel)
1267
- }
1268
-
1269
- // Continue block (step + loop back)
1270
- this.builder.startBlock(continueLabel)
1271
- this.lowerExpr(stmt.step)
1272
- this.builder.emitJump(checkLabel)
1273
-
1274
- // Pop loop context
1275
- this.loopStack.pop()
1276
-
1277
- // Exit block
1278
- this.builder.startBlock(exitLabel)
1279
- }
1280
-
1281
- private lowerForRangeStmt(stmt: Extract<Stmt, { kind: 'for_range' }>): void {
1282
- const loopVar = this.fnVar(stmt.varName)
1283
- const subFnName = `${this.currentFn}/__for_${this.foreachCounter++}`
1284
-
1285
- // Initialize loop variable
1286
- this.varMap.set(stmt.varName, loopVar)
1287
- const startVal = this.lowerExpr(stmt.start)
1288
- if (startVal.kind === 'const') {
1289
- this.builder.emitRaw(`scoreboard players set ${loopVar} ${LOWERING_OBJ} ${startVal.value}`)
1290
- } else if (startVal.kind === 'var') {
1291
- this.builder.emitRaw(`scoreboard players operation ${loopVar} ${LOWERING_OBJ} = ${startVal.name} ${LOWERING_OBJ}`)
1292
- }
1293
-
1294
- // Call loop function
1295
- this.builder.emitRaw(`function ${this.namespace}:${subFnName}`)
1296
-
1297
- // Generate loop sub-function
1298
- const savedBuilder = this.builder
1299
- const savedVarMap = new Map(this.varMap)
1300
- const savedContext = this.currentContext
1301
- const savedBlockPosVars = new Map(this.blockPosVars)
1302
-
1303
- this.builder = new LoweringBuilder()
1304
- this.varMap = new Map(savedVarMap)
1305
- this.currentContext = savedContext
1306
- this.blockPosVars = new Map(savedBlockPosVars)
1307
-
1308
- this.builder.startBlock('entry')
1309
-
1310
- // Body
1311
- this.lowerBlock(stmt.body)
1312
-
1313
- // Increment
1314
- this.builder.emitRaw(`scoreboard players add ${loopVar} ${LOWERING_OBJ} 1`)
1315
-
1316
- // Loop condition: execute if score matches ..<end-1> run function
1317
- const endVal = this.lowerExpr(stmt.end)
1318
- const endNum = endVal.kind === 'const' ? endVal.value - 1 : '?'
1319
- this.builder.emitRaw(`execute if score ${loopVar} ${LOWERING_OBJ} matches ..${endNum} run function ${this.namespace}:${subFnName}`)
1320
-
1321
- if (!this.builder.isBlockSealed()) {
1322
- this.builder.emitReturn()
1323
- }
1324
-
1325
- const subFn = this.builder.build(subFnName, [], false)
1326
- this.functions.push(subFn)
1327
-
1328
- // Restore
1329
- this.builder = savedBuilder
1330
- this.varMap = savedVarMap
1331
- this.currentContext = savedContext
1332
- this.blockPosVars = savedBlockPosVars
1333
- }
1334
-
1335
- private lowerForeachStmt(stmt: Extract<Stmt, { kind: 'foreach' }>): void {
1336
- if (stmt.iterable.kind !== 'selector') {
1337
- this.lowerArrayForeachStmt(stmt)
1338
- return
1339
- }
1340
-
1341
- // Extract body into a separate function
1342
- const subFnName = `${this.currentFn}/foreach_${this.foreachCounter++}`
1343
- const selector = this.exprToString(stmt.iterable)
1344
-
1345
- // Emit execute as ... [context modifiers] run function ...
1346
- const execContext = stmt.executeContext ? ` ${stmt.executeContext}` : ''
1347
- this.builder.emitRaw(`execute as ${selector}${execContext} run function ${this.namespace}:${subFnName}`)
1348
-
1349
- // Create the sub-function
1350
- const savedBuilder = this.builder
1351
- const savedVarMap = new Map(this.varMap)
1352
- const savedContext = this.currentContext
1353
- const savedBlockPosVars = new Map(this.blockPosVars)
1354
-
1355
- this.builder = new LoweringBuilder()
1356
- this.varMap = new Map(savedVarMap)
1357
- this.currentContext = { binding: stmt.binding }
1358
- this.blockPosVars = new Map(savedBlockPosVars)
1359
-
1360
- // In foreach body, the binding maps to @s
1361
- this.varMap.set(stmt.binding, '@s')
1362
-
1363
- // Track entity context for type narrowing
1364
- const selectorEntityType = getBaseSelectorType(selector)
1365
- if (selectorEntityType) {
1366
- this.entityContextStack.push(selectorEntityType)
1367
- }
1368
-
1369
- this.builder.startBlock('entry')
1370
- this.lowerBlock(stmt.body)
1371
- if (!this.builder.isBlockSealed()) {
1372
- this.builder.emitReturn()
1373
- }
1374
-
1375
- if (selectorEntityType) {
1376
- this.entityContextStack.pop()
1377
- }
1378
-
1379
- const subFn = this.builder.build(subFnName, [], false)
1380
- this.functions.push(subFn)
1381
-
1382
- // Restore
1383
- this.builder = savedBuilder
1384
- this.varMap = savedVarMap
1385
- this.currentContext = savedContext
1386
- this.blockPosVars = savedBlockPosVars
1387
- }
1388
-
1389
- private lowerMatchStmt(stmt: Extract<Stmt, { kind: 'match' }>): void {
1390
- const subject = this.operandToVar(this.lowerExpr(stmt.expr))
1391
- const matchedVar = this.builder.freshTemp()
1392
- this.builder.emitAssign(matchedVar, { kind: 'const', value: 0 })
1393
-
1394
- let defaultArm: { pattern: Expr | null; body: Block } | null = null
1395
-
1396
- for (const arm of stmt.arms) {
1397
- if (arm.pattern === null) {
1398
- defaultArm = arm
1399
- continue
1400
- }
1401
-
1402
- // Handle range patterns specially
1403
- let matchCondition: string
1404
- if (arm.pattern.kind === 'range_lit') {
1405
- const range = arm.pattern.range
1406
- if (range.min !== undefined && range.max !== undefined) {
1407
- matchCondition = `${range.min}..${range.max}`
1408
- } else if (range.min !== undefined) {
1409
- matchCondition = `${range.min}..`
1410
- } else if (range.max !== undefined) {
1411
- matchCondition = `..${range.max}`
1412
- } else {
1413
- matchCondition = '0..' // Match any
1414
- }
1415
- } else {
1416
- const patternValue = this.lowerExpr(arm.pattern)
1417
- if (patternValue.kind !== 'const') {
1418
- throw new Error('Match patterns must lower to compile-time constants')
1419
- }
1420
- matchCondition = String(patternValue.value)
1421
- }
1422
-
1423
- const subFnName = `${this.currentFn}/match_${this.foreachCounter++}`
1424
- this.builder.emitRaw(`execute if score ${matchedVar} ${LOWERING_OBJ} matches ..0 if score ${subject} ${LOWERING_OBJ} matches ${matchCondition} run function ${this.namespace}:${subFnName}`)
1425
- this.emitMatchArmSubFunction(subFnName, matchedVar, arm.body, true)
1426
- }
1427
-
1428
- if (defaultArm) {
1429
- const subFnName = `${this.currentFn}/match_${this.foreachCounter++}`
1430
- this.builder.emitRaw(`execute if score ${matchedVar} ${LOWERING_OBJ} matches ..0 run function ${this.namespace}:${subFnName}`)
1431
- this.emitMatchArmSubFunction(subFnName, matchedVar, defaultArm.body, false)
1432
- }
1433
- }
1434
-
1435
- private emitMatchArmSubFunction(name: string, matchedVar: string, body: Block, setMatched: boolean): void {
1436
- const savedBuilder = this.builder
1437
- const savedVarMap = new Map(this.varMap)
1438
- const savedContext = this.currentContext
1439
- const savedBlockPosVars = new Map(this.blockPosVars)
1440
-
1441
- this.builder = new LoweringBuilder()
1442
- this.varMap = new Map(savedVarMap)
1443
- this.currentContext = savedContext
1444
- this.blockPosVars = new Map(savedBlockPosVars)
1445
-
1446
- this.builder.startBlock('entry')
1447
- if (setMatched) {
1448
- this.builder.emitRaw(`scoreboard players set ${matchedVar} ${LOWERING_OBJ} 1`)
1449
- }
1450
- this.lowerBlock(body)
1451
- if (!this.builder.isBlockSealed()) {
1452
- this.builder.emitReturn()
1453
- }
1454
-
1455
- this.functions.push(this.builder.build(name, [], false))
1456
-
1457
- this.builder = savedBuilder
1458
- this.varMap = savedVarMap
1459
- this.currentContext = savedContext
1460
- this.blockPosVars = savedBlockPosVars
1461
- }
1462
-
1463
- private lowerArrayForeachStmt(stmt: Extract<Stmt, { kind: 'foreach' }>): void {
1464
- const arrayName = this.getArrayStorageName(stmt.iterable)
1465
- if (!arrayName) {
1466
- this.builder.emitRaw('# Unsupported foreach iterable')
1467
- return
1468
- }
1469
-
1470
- const arrayType = this.inferExprType(stmt.iterable)
1471
- const bindingVar = this.fnVar(stmt.binding)
1472
- const indexVar = this.builder.freshTemp()
1473
- const lengthVar = this.builder.freshTemp()
1474
- const condVar = this.builder.freshTemp()
1475
- const oneVar = this.builder.freshTemp()
1476
-
1477
- const savedBinding = this.varMap.get(stmt.binding)
1478
- const savedType = this.varTypes.get(stmt.binding)
1479
-
1480
- this.varMap.set(stmt.binding, bindingVar)
1481
- if (arrayType?.kind === 'array') {
1482
- this.varTypes.set(stmt.binding, arrayType.elem)
1483
- }
1484
-
1485
- this.builder.emitAssign(indexVar, { kind: 'const', value: 0 })
1486
- this.builder.emitAssign(oneVar, { kind: 'const', value: 1 })
1487
- this.builder.emitRaw(`execute store result score ${lengthVar} ${LOWERING_OBJ} run data get storage rs:heap ${arrayName}`)
1488
-
1489
- const checkLabel = this.builder.freshLabel('foreach_array_check')
1490
- const bodyLabel = this.builder.freshLabel('foreach_array_body')
1491
- const exitLabel = this.builder.freshLabel('foreach_array_exit')
1492
-
1493
- this.builder.emitJump(checkLabel)
1494
-
1495
- this.builder.startBlock(checkLabel)
1496
- this.builder.emitCmp(condVar, { kind: 'var', name: indexVar }, '<', { kind: 'var', name: lengthVar })
1497
- this.builder.emitJumpIf(condVar, bodyLabel, exitLabel)
1498
-
1499
- this.builder.startBlock(bodyLabel)
1500
- const element = this.readArrayElement(arrayName, { kind: 'var', name: indexVar })
1501
- this.builder.emitAssign(bindingVar, element)
1502
- this.lowerBlock(stmt.body)
1503
- if (!this.builder.isBlockSealed()) {
1504
- this.builder.emitRaw(`scoreboard players operation ${indexVar} ${LOWERING_OBJ} += ${oneVar} ${LOWERING_OBJ}`)
1505
- this.builder.emitJump(checkLabel)
1506
- }
1507
-
1508
- this.builder.startBlock(exitLabel)
1509
-
1510
- if (savedBinding) {
1511
- this.varMap.set(stmt.binding, savedBinding)
1512
- } else {
1513
- this.varMap.delete(stmt.binding)
1514
- }
1515
-
1516
- if (savedType) {
1517
- this.varTypes.set(stmt.binding, savedType)
1518
- } else {
1519
- this.varTypes.delete(stmt.binding)
1520
- }
1521
- }
1522
-
1523
- private lowerAsBlockStmt(stmt: Extract<Stmt, { kind: 'as_block' }>): void {
1524
- const selector = this.selectorToString(stmt.selector)
1525
- const subFnName = `${this.currentFn}/as_${this.foreachCounter++}`
1526
-
1527
- // Check for impossible type assertions (W_IMPOSSIBLE_AS)
1528
- const innerType = getBaseSelectorType(selector)
1529
- const outerType = this.currentEntityContext()
1530
- if (innerType && outerType !== 'Entity' && innerType !== 'Entity' && !areCompatibleTypes(outerType, innerType)) {
1531
- this.warnings.push({
1532
- message: `Impossible type assertion: @s is ${outerType} but as-block targets ${innerType}`,
1533
- code: 'W_IMPOSSIBLE_AS',
1534
- line: stmt.span?.line,
1535
- col: stmt.span?.col,
1536
- })
1537
- }
1538
-
1539
- this.builder.emitRaw(`execute as ${selector} run function ${this.namespace}:${subFnName}`)
1540
-
1541
- // Create sub-function
1542
- const savedBuilder = this.builder
1543
- const savedVarMap = new Map(this.varMap)
1544
- const savedBlockPosVars = new Map(this.blockPosVars)
1545
-
1546
- this.builder = new LoweringBuilder()
1547
- this.varMap = new Map(savedVarMap)
1548
- this.blockPosVars = new Map(savedBlockPosVars)
1549
-
1550
- // Track entity context inside as-block
1551
- if (innerType) {
1552
- this.entityContextStack.push(innerType)
1553
- }
1554
-
1555
- this.builder.startBlock('entry')
1556
- this.lowerBlock(stmt.body)
1557
- if (!this.builder.isBlockSealed()) {
1558
- this.builder.emitReturn()
1559
- }
1560
-
1561
- if (innerType) {
1562
- this.entityContextStack.pop()
1563
- }
1564
-
1565
- const subFn = this.builder.build(subFnName, [], false)
1566
- this.functions.push(subFn)
1567
-
1568
- this.builder = savedBuilder
1569
- this.varMap = savedVarMap
1570
- this.blockPosVars = savedBlockPosVars
1571
- }
1572
-
1573
- private lowerAtBlockStmt(stmt: Extract<Stmt, { kind: 'at_block' }>): void {
1574
- const selector = this.selectorToString(stmt.selector)
1575
- const subFnName = `${this.currentFn}/at_${this.foreachCounter++}`
1576
-
1577
- this.builder.emitRaw(`execute at ${selector} run function ${this.namespace}:${subFnName}`)
1578
-
1579
- // Create sub-function
1580
- const savedBuilder = this.builder
1581
- const savedVarMap = new Map(this.varMap)
1582
- const savedBlockPosVars = new Map(this.blockPosVars)
1583
-
1584
- this.builder = new LoweringBuilder()
1585
- this.varMap = new Map(savedVarMap)
1586
- this.blockPosVars = new Map(savedBlockPosVars)
1587
-
1588
- this.builder.startBlock('entry')
1589
- this.lowerBlock(stmt.body)
1590
- if (!this.builder.isBlockSealed()) {
1591
- this.builder.emitReturn()
1592
- }
1593
-
1594
- const subFn = this.builder.build(subFnName, [], false)
1595
- this.functions.push(subFn)
1596
-
1597
- this.builder = savedBuilder
1598
- this.varMap = savedVarMap
1599
- this.blockPosVars = savedBlockPosVars
1600
- }
1601
-
1602
- private lowerAsAtStmt(stmt: Extract<Stmt, { kind: 'as_at' }>): void {
1603
- const asSel = this.selectorToString(stmt.as_sel)
1604
- const atSel = this.selectorToString(stmt.at_sel)
1605
- const subFnName = `${this.currentFn}/as_at_${this.foreachCounter++}`
1606
-
1607
- this.builder.emitRaw(`execute as ${asSel} at ${atSel} run function ${this.namespace}:${subFnName}`)
1608
-
1609
- // Create sub-function
1610
- const savedBuilder = this.builder
1611
- const savedVarMap = new Map(this.varMap)
1612
- const savedBlockPosVars = new Map(this.blockPosVars)
1613
-
1614
- this.builder = new LoweringBuilder()
1615
- this.varMap = new Map(savedVarMap)
1616
- this.blockPosVars = new Map(savedBlockPosVars)
1617
-
1618
- this.builder.startBlock('entry')
1619
- this.lowerBlock(stmt.body)
1620
- if (!this.builder.isBlockSealed()) {
1621
- this.builder.emitReturn()
1622
- }
1623
-
1624
- const subFn = this.builder.build(subFnName, [], false)
1625
- this.functions.push(subFn)
1626
-
1627
- this.builder = savedBuilder
1628
- this.varMap = savedVarMap
1629
- this.blockPosVars = savedBlockPosVars
1630
- }
1631
-
1632
- private lowerExecuteStmt(stmt: Extract<Stmt, { kind: 'execute' }>): void {
1633
- // Build the execute prefix from subcommands
1634
- const parts: string[] = ['execute']
1635
- for (const sub of stmt.subcommands) {
1636
- switch (sub.kind) {
1637
- // Context modifiers
1638
- case 'as':
1639
- parts.push(`as ${this.selectorToString(sub.selector)}`)
1640
- break
1641
- case 'at':
1642
- parts.push(`at ${this.selectorToString(sub.selector)}`)
1643
- break
1644
- case 'positioned':
1645
- parts.push(`positioned ${sub.x} ${sub.y} ${sub.z}`)
1646
- break
1647
- case 'positioned_as':
1648
- parts.push(`positioned as ${this.selectorToString(sub.selector)}`)
1649
- break
1650
- case 'rotated':
1651
- parts.push(`rotated ${sub.yaw} ${sub.pitch}`)
1652
- break
1653
- case 'rotated_as':
1654
- parts.push(`rotated as ${this.selectorToString(sub.selector)}`)
1655
- break
1656
- case 'facing':
1657
- parts.push(`facing ${sub.x} ${sub.y} ${sub.z}`)
1658
- break
1659
- case 'facing_entity':
1660
- parts.push(`facing entity ${this.selectorToString(sub.selector)} ${sub.anchor}`)
1661
- break
1662
- case 'anchored':
1663
- parts.push(`anchored ${sub.anchor}`)
1664
- break
1665
- case 'align':
1666
- parts.push(`align ${sub.axes}`)
1667
- break
1668
- case 'in':
1669
- parts.push(`in ${sub.dimension}`)
1670
- break
1671
- case 'on':
1672
- parts.push(`on ${sub.relation}`)
1673
- break
1674
- case 'summon':
1675
- parts.push(`summon ${sub.entity}`)
1676
- break
1677
- // Conditions
1678
- case 'if_entity':
1679
- if (sub.selector) {
1680
- parts.push(`if entity ${this.selectorToString(sub.selector)}`)
1681
- } else if (sub.varName) {
1682
- const sel: EntitySelector = { kind: '@s', filters: sub.filters }
1683
- parts.push(`if entity ${this.selectorToString(sel)}`)
1684
- }
1685
- break
1686
- case 'unless_entity':
1687
- if (sub.selector) {
1688
- parts.push(`unless entity ${this.selectorToString(sub.selector)}`)
1689
- } else if (sub.varName) {
1690
- const sel: EntitySelector = { kind: '@s', filters: sub.filters }
1691
- parts.push(`unless entity ${this.selectorToString(sel)}`)
1692
- }
1693
- break
1694
- case 'if_block':
1695
- parts.push(`if block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
1696
- break
1697
- case 'unless_block':
1698
- parts.push(`unless block ${sub.pos[0]} ${sub.pos[1]} ${sub.pos[2]} ${sub.block}`)
1699
- break
1700
- case 'if_score':
1701
- parts.push(`if score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
1702
- break
1703
- case 'unless_score':
1704
- parts.push(`unless score ${sub.target} ${sub.targetObj} ${sub.op} ${sub.source} ${sub.sourceObj}`)
1705
- break
1706
- case 'if_score_range':
1707
- parts.push(`if score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
1708
- break
1709
- case 'unless_score_range':
1710
- parts.push(`unless score ${sub.target} ${sub.targetObj} matches ${sub.range}`)
1711
- break
1712
- // Store
1713
- case 'store_result':
1714
- parts.push(`store result score ${sub.target} ${sub.targetObj}`)
1715
- break
1716
- case 'store_success':
1717
- parts.push(`store success score ${sub.target} ${sub.targetObj}`)
1718
- break
1719
- }
1720
- }
1721
-
1722
- const subFnName = `${this.currentFn}/exec_${this.foreachCounter++}`
1723
- this.builder.emitRaw(`${parts.join(' ')} run function ${this.namespace}:${subFnName}`)
1724
-
1725
- // Create sub-function for the body
1726
- const savedBuilder = this.builder
1727
- const savedVarMap = new Map(this.varMap)
1728
- const savedBlockPosVars = new Map(this.blockPosVars)
1729
-
1730
- this.builder = new LoweringBuilder()
1731
- this.varMap = new Map(savedVarMap)
1732
- this.blockPosVars = new Map(savedBlockPosVars)
1733
-
1734
- this.builder.startBlock('entry')
1735
- this.lowerBlock(stmt.body)
1736
- if (!this.builder.isBlockSealed()) {
1737
- this.builder.emitReturn()
1738
- }
1739
-
1740
- const subFn = this.builder.build(subFnName, [], false)
1741
- this.functions.push(subFn)
1742
-
1743
- this.builder = savedBuilder
1744
- this.varMap = savedVarMap
1745
- this.blockPosVars = savedBlockPosVars
1746
- }
1747
-
1748
- // -------------------------------------------------------------------------
1749
- // Expression Lowering
1750
- // -------------------------------------------------------------------------
1751
-
1752
- private lowerExpr(expr: Expr): Operand {
1753
- switch (expr.kind) {
1754
- case 'int_lit':
1755
- return { kind: 'const', value: expr.value }
1756
-
1757
- case 'float_lit':
1758
- // Float stored as fixed-point × 1000
1759
- return { kind: 'const', value: Math.round(expr.value * 1000) }
1760
-
1761
- case 'byte_lit':
1762
- return { kind: 'const', value: expr.value }
1763
-
1764
- case 'short_lit':
1765
- return { kind: 'const', value: expr.value }
1766
-
1767
- case 'long_lit':
1768
- return { kind: 'const', value: expr.value }
1769
-
1770
- case 'double_lit':
1771
- return { kind: 'const', value: Math.round(expr.value * 1000) }
1772
-
1773
- case 'bool_lit':
1774
- return { kind: 'const', value: expr.value ? 1 : 0 }
1775
-
1776
- case 'str_lit':
1777
- // Strings are handled inline in builtins
1778
- return { kind: 'const', value: 0 } // Placeholder
1779
-
1780
- case 'mc_name':
1781
- // MC names (#health, #red) treated as string constants
1782
- return { kind: 'const', value: 0 } // Handled inline in exprToString
1783
-
1784
- case 'str_interp':
1785
- case 'f_string':
1786
- // Interpolated strings are handled inline in message builtins.
1787
- return { kind: 'const', value: 0 }
1788
-
1789
- case 'range_lit':
1790
- // Ranges are handled in context (selectors, etc.)
1791
- return { kind: 'const', value: 0 }
1792
-
1793
- case 'blockpos':
1794
- return { kind: 'const', value: 0 }
1795
-
1796
- case 'ident': {
1797
- const constValue = this.constValues.get(expr.name)
1798
- if (constValue) {
1799
- return this.lowerConstLiteral(constValue)
1800
- }
1801
- const mapped = this.varMap.get(expr.name)
1802
- if (mapped) {
1803
- // Check if it's a selector reference (like @s)
1804
- if (mapped.startsWith('@')) {
1805
- return { kind: 'var', name: mapped }
1806
- }
1807
- return { kind: 'var', name: mapped }
1808
- }
1809
- return { kind: 'var', name: `$${expr.name}` }
1810
- }
1811
-
1812
- case 'member':
1813
- if (expr.obj.kind === 'ident' && this.enumDefs.has(expr.obj.name)) {
1814
- const variants = this.enumDefs.get(expr.obj.name)!
1815
- const value = variants.get(expr.field)
1816
- if (value === undefined) {
1817
- throw new Error(`Unknown enum variant ${expr.obj.name}.${expr.field}`)
1818
- }
1819
- return { kind: 'const', value }
1820
- }
1821
- return this.lowerMemberExpr(expr)
1822
-
1823
- case 'selector':
1824
- // Selectors are handled inline in builtins
1825
- return { kind: 'var', name: this.selectorToString(expr.sel) }
1826
-
1827
- case 'binary':
1828
- return this.lowerBinaryExpr(expr)
1829
-
1830
- case 'is_check':
1831
- throw new DiagnosticError(
1832
- 'LoweringError',
1833
- "'is' checks are only supported as if conditions",
1834
- expr.span ?? { line: 0, col: 0 }
1835
- )
1836
-
1837
- case 'unary':
1838
- return this.lowerUnaryExpr(expr)
1839
-
1840
- case 'assign':
1841
- return this.lowerAssignExpr(expr)
1842
-
1843
- case 'call':
1844
- return this.lowerCallExpr(expr)
1845
-
1846
- case 'static_call':
1847
- return this.lowerStaticCallExpr(expr)
1848
-
1849
- case 'invoke':
1850
- return this.lowerInvokeExpr(expr)
1851
-
1852
- case 'member_assign':
1853
- return this.lowerMemberAssign(expr)
1854
-
1855
- case 'index':
1856
- return this.lowerIndexExpr(expr)
1857
-
1858
- case 'struct_lit':
1859
- // Struct literals should be handled in let statement
1860
- return { kind: 'const', value: 0 }
1861
-
1862
- case 'array_lit':
1863
- // Array literals should be handled in let statement
1864
- return { kind: 'const', value: 0 }
1865
-
1866
- case 'lambda':
1867
- throw new Error('Lambda expressions must be used in a function context')
1868
- }
1869
-
1870
- throw new Error(`Unhandled expression kind: ${(expr as { kind: string }).kind}`)
1871
- }
1872
-
1873
- private lowerMemberExpr(expr: Extract<Expr, { kind: 'member' }>): Operand {
1874
- // Check if this is a struct field access
1875
- if (expr.obj.kind === 'ident') {
1876
- const varType = this.varTypes.get(expr.obj.name)
1877
-
1878
- // Check for world object handle (entity selector)
1879
- const mapped = this.varMap.get(expr.obj.name)
1880
- if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
1881
- // World object field access → scoreboard get
1882
- const dst = this.builder.freshTemp()
1883
- this.builder.emitRaw(`scoreboard players operation ${dst} ${LOWERING_OBJ} = ${mapped} ${LOWERING_OBJ}`)
1884
- return { kind: 'var', name: dst }
1885
- }
1886
-
1887
- if (varType?.kind === 'struct') {
1888
- const structName = varType.name.toLowerCase()
1889
- const path = `rs:heap ${structName}_${expr.obj.name}.${expr.field}`
1890
- const dst = this.builder.freshTemp()
1891
- // Read from NBT storage into scoreboard
1892
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get storage ${path}`)
1893
- return { kind: 'var', name: dst }
1894
- }
1895
-
1896
- // Array length property
1897
- if (varType?.kind === 'array' && expr.field === 'len') {
1898
- const dst = this.builder.freshTemp()
1899
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get storage rs:heap ${expr.obj.name}`)
1900
- return { kind: 'var', name: dst }
1901
- }
1902
- }
1903
-
1904
- // Default behavior: simple member access
1905
- return { kind: 'var', name: `$${(expr.obj as any).name}_${expr.field}` }
1906
- }
1907
-
1908
- private lowerMemberAssign(expr: Extract<Expr, { kind: 'member_assign' }>): Operand {
1909
- if (expr.obj.kind === 'ident') {
1910
- const varType = this.varTypes.get(expr.obj.name)
1911
-
1912
- // Check for world object handle
1913
- const mapped = this.varMap.get(expr.obj.name)
1914
- if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
1915
- const value = this.lowerExpr(expr.value)
1916
- if (expr.op === '=') {
1917
- if (value.kind === 'const') {
1918
- this.builder.emitRaw(`scoreboard players set ${mapped} ${LOWERING_OBJ} ${value.value}`)
1919
- } else if (value.kind === 'var') {
1920
- this.builder.emitRaw(`scoreboard players operation ${mapped} ${LOWERING_OBJ} = ${value.name} ${LOWERING_OBJ}`)
1921
- }
1922
- } else {
1923
- // Compound assignment
1924
- const binOp = expr.op.slice(0, -1)
1925
- const opMap: Record<string, string> = { '+': '+=', '-': '-=', '*': '*=', '/': '/=', '%': '%=' }
1926
- if (value.kind === 'const') {
1927
- const constTemp = this.builder.freshTemp()
1928
- this.builder.emitAssign(constTemp, value)
1929
- this.builder.emitRaw(`scoreboard players operation ${mapped} ${LOWERING_OBJ} ${opMap[binOp]} ${constTemp} ${LOWERING_OBJ}`)
1930
- } else if (value.kind === 'var') {
1931
- this.builder.emitRaw(`scoreboard players operation ${mapped} ${LOWERING_OBJ} ${opMap[binOp]} ${value.name} ${LOWERING_OBJ}`)
1932
- }
1933
- }
1934
- return { kind: 'const', value: 0 }
1935
- }
1936
-
1937
- if (varType?.kind === 'struct') {
1938
- const structName = varType.name.toLowerCase()
1939
- const path = `rs:heap ${structName}_${expr.obj.name}.${expr.field}`
1940
- const value = this.lowerExpr(expr.value)
1941
-
1942
- if (expr.op === '=') {
1943
- if (value.kind === 'const') {
1944
- this.builder.emitRaw(`data modify storage ${path} set value ${value.value}`)
1945
- } else if (value.kind === 'var') {
1946
- this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${value.name} ${LOWERING_OBJ}`)
1947
- }
1948
- } else {
1949
- // Compound assignment: read, modify, write back
1950
- const dst = this.builder.freshTemp()
1951
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get storage ${path}`)
1952
- const binOp = expr.op.slice(0, -1)
1953
- this.builder.emitBinop(dst, { kind: 'var', name: dst }, binOp as any, value)
1954
- this.builder.emitRaw(`execute store result storage ${path} int 1 run scoreboard players get ${dst} ${LOWERING_OBJ}`)
1955
- }
1956
- return { kind: 'const', value: 0 }
1957
- }
1958
- }
1959
-
1960
- // Default: simple assignment
1961
- const varName = `$${(expr.obj as any).name}_${expr.field}`
1962
- const value = this.lowerExpr(expr.value)
1963
- this.builder.emitAssign(varName, value)
1964
- return { kind: 'var', name: varName }
1965
- }
1966
-
1967
- private lowerIndexExpr(expr: Extract<Expr, { kind: 'index' }>): Operand {
1968
- const arrayName = this.getArrayStorageName(expr.obj)
1969
- if (arrayName) {
1970
- return this.readArrayElement(arrayName, this.lowerExpr(expr.index))
1971
- }
1972
- return { kind: 'const', value: 0 }
1973
- }
1974
-
1975
- private lowerBinaryExpr(expr: Extract<Expr, { kind: 'binary' }>): Operand {
1976
- const left = this.lowerExpr(expr.left)
1977
- const right = this.lowerExpr(expr.right)
1978
- const dst = this.builder.freshTemp()
1979
-
1980
- if (['&&', '||'].includes(expr.op)) {
1981
- // Logical operators need special handling
1982
- if (expr.op === '&&') {
1983
- // Short-circuit AND
1984
- this.builder.emitAssign(dst, left)
1985
- const rightVar = this.operandToVar(right)
1986
- // dst = dst && right → if dst != 0 then dst = right
1987
- this.builder.emitRaw(`execute if score ${dst} ${LOWERING_OBJ} matches 1.. run scoreboard players operation ${dst} ${LOWERING_OBJ} = ${rightVar} ${LOWERING_OBJ}`)
1988
- } else {
1989
- // Short-circuit OR
1990
- this.builder.emitAssign(dst, left)
1991
- const rightVar = this.operandToVar(right)
1992
- // dst = dst || right → if dst == 0 then dst = right
1993
- this.builder.emitRaw(`execute if score ${dst} ${LOWERING_OBJ} matches ..0 run scoreboard players operation ${dst} ${LOWERING_OBJ} = ${rightVar} ${LOWERING_OBJ}`)
1994
- }
1995
- return { kind: 'var', name: dst }
1996
- }
1997
-
1998
- if (['==', '!=', '<', '<=', '>', '>='].includes(expr.op)) {
1999
- this.builder.emitCmp(dst, left, expr.op as CmpOp, right)
2000
- } else {
2001
- // Check if this is float arithmetic
2002
- const isFloatOp = this.isFloatExpr(expr.left) || this.isFloatExpr(expr.right)
2003
-
2004
- if (isFloatOp && (expr.op === '*' || expr.op === '/')) {
2005
- // Float multiplication: a * b / 1000
2006
- // Float division: a * 1000 / b
2007
- if (expr.op === '*') {
2008
- this.builder.emitBinop(dst, left, '*', right)
2009
- // Divide by 1000 to correct for double scaling
2010
- const constDiv = this.builder.freshTemp()
2011
- this.builder.emitAssign(constDiv, { kind: 'const', value: 1000 })
2012
- this.builder.emitRaw(`scoreboard players operation ${dst} ${LOWERING_OBJ} /= ${constDiv} ${LOWERING_OBJ}`)
2013
- } else {
2014
- // Division: a * 1000 / b
2015
- const constMul = this.builder.freshTemp()
2016
- this.builder.emitAssign(constMul, { kind: 'const', value: 1000 })
2017
- this.builder.emitAssign(dst, left)
2018
- this.builder.emitRaw(`scoreboard players operation ${dst} ${LOWERING_OBJ} *= ${constMul} ${LOWERING_OBJ}`)
2019
- const rightVar = this.operandToVar(right)
2020
- this.builder.emitRaw(`scoreboard players operation ${dst} ${LOWERING_OBJ} /= ${rightVar} ${LOWERING_OBJ}`)
2021
- }
2022
- return { kind: 'var', name: dst }
2023
- }
2024
-
2025
- this.builder.emitBinop(dst, left, expr.op as BinOp, right)
2026
- }
2027
-
2028
- return { kind: 'var', name: dst }
2029
- }
2030
-
2031
- private isFloatExpr(expr: Expr): boolean {
2032
- if (expr.kind === 'float_lit') return true
2033
- if (expr.kind === 'ident') {
2034
- return this.floatVars.has(expr.name)
2035
- }
2036
- if (expr.kind === 'binary') {
2037
- return this.isFloatExpr(expr.left) || this.isFloatExpr(expr.right)
2038
- }
2039
- return false
2040
- }
2041
-
2042
- private lowerUnaryExpr(expr: Extract<Expr, { kind: 'unary' }>): Operand {
2043
- const operand = this.lowerExpr(expr.operand)
2044
- const dst = this.builder.freshTemp()
2045
-
2046
- if (expr.op === '!') {
2047
- // Logical NOT: dst = (operand == 0) ? 1 : 0
2048
- this.builder.emitCmp(dst, operand, '==', { kind: 'const', value: 0 })
2049
- } else if (expr.op === '-') {
2050
- // Negation: dst = 0 - operand
2051
- this.builder.emitBinop(dst, { kind: 'const', value: 0 }, '-', operand)
2052
- }
2053
-
2054
- return { kind: 'var', name: dst }
2055
- }
2056
-
2057
- private lowerAssignExpr(expr: Extract<Expr, { kind: 'assign' }>): Operand {
2058
- // Check for const reassignment (both compile-time consts and immutable globals)
2059
- if (this.constValues.has(expr.target)) {
2060
- throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
2061
- }
2062
- const globalInfo = this.globalNames.get(expr.target)
2063
- if (globalInfo && !globalInfo.mutable) {
2064
- throw new DiagnosticError('LoweringError', `Cannot assign to constant '${expr.target}'`, getSpan(expr) ?? { line: 1, col: 1 })
2065
- }
2066
-
2067
- const blockPosValue = this.resolveBlockPosExpr(expr.value)
2068
- if (blockPosValue) {
2069
- this.blockPosVars.set(expr.target, blockPosValue)
2070
- return { kind: 'const', value: 0 }
2071
- }
2072
-
2073
- this.blockPosVars.delete(expr.target)
2074
- const targetType = this.varTypes.get(expr.target)
2075
- if (targetType?.kind === 'named' && targetType.name === 'string' && this.storeStringValue(expr.target, expr.value)) {
2076
- return { kind: 'const', value: 0 }
2077
- }
2078
- const varName = this.varMap.get(expr.target) ?? `$${expr.target}`
2079
- const value = this.lowerExpr(expr.value)
2080
-
2081
- if (expr.op === '=') {
2082
- this.builder.emitAssign(varName, value)
2083
- } else {
2084
- // Compound assignment
2085
- const binOp = expr.op.slice(0, -1) as BinOp // Remove '='
2086
- const dst = this.builder.freshTemp()
2087
- this.builder.emitBinop(dst, { kind: 'var', name: varName }, binOp, value)
2088
- this.builder.emitAssign(varName, { kind: 'var', name: dst })
2089
- }
2090
-
2091
- return { kind: 'var', name: varName }
2092
- }
2093
-
2094
- private lowerCallExpr(expr: Extract<Expr, { kind: 'call' }>): Operand {
2095
- if (expr.fn === 'str_len') {
2096
- const storagePath = this.getStringStoragePath(expr.args[0])
2097
- if (storagePath) {
2098
- const dst = this.builder.freshTemp()
2099
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get storage ${storagePath}`)
2100
- return { kind: 'var', name: dst }
2101
- }
2102
-
2103
- const staticString = this.resolveStaticString(expr.args[0])
2104
- if (staticString !== null) {
2105
- return { kind: 'const', value: Array.from(staticString).length }
2106
- } else {
2107
- const dst = this.builder.freshTemp()
2108
- this.builder.emitAssign(dst, { kind: 'const', value: 0 })
2109
- return { kind: 'var', name: dst }
2110
- }
2111
- }
2112
-
2113
- // Check for builtin
2114
- if (expr.fn in BUILTINS) {
2115
- return this.lowerBuiltinCall(expr.fn, expr.args, getSpan(expr))
2116
- }
2117
-
2118
- // Handle entity methods: __entity_tag, __entity_untag, __entity_has_tag
2119
- if (expr.fn === '__entity_tag') {
2120
- const entity = this.exprToString(expr.args[0])
2121
- const tagName = this.exprToString(expr.args[1])
2122
- this.builder.emitRaw(`tag ${entity} add ${tagName}`)
2123
- return { kind: 'const', value: 0 }
2124
- }
2125
-
2126
- if (expr.fn === '__entity_untag') {
2127
- const entity = this.exprToString(expr.args[0])
2128
- const tagName = this.exprToString(expr.args[1])
2129
- this.builder.emitRaw(`tag ${entity} remove ${tagName}`)
2130
- return { kind: 'const', value: 0 }
2131
- }
2132
-
2133
- if (expr.fn === '__entity_has_tag') {
2134
- const entity = this.exprToString(expr.args[0])
2135
- const tagName = this.exprToString(expr.args[1])
2136
- const dst = this.builder.freshTemp()
2137
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} if entity ${entity}[tag=${tagName}]`)
2138
- return { kind: 'var', name: dst }
2139
- }
2140
-
2141
- // Handle array push
2142
- if (expr.fn === '__array_push') {
2143
- const arrExpr = expr.args[0]
2144
- const valueExpr = expr.args[1]
2145
- const arrName = this.getArrayStorageName(arrExpr)
2146
- if (arrName) {
2147
- const value = this.lowerExpr(valueExpr)
2148
- if (value.kind === 'const') {
2149
- this.builder.emitRaw(`data modify storage rs:heap ${arrName} append value ${value.value}`)
2150
- } else if (value.kind === 'var') {
2151
- this.builder.emitRaw(`data modify storage rs:heap ${arrName} append value 0`)
2152
- this.builder.emitRaw(`execute store result storage rs:heap ${arrName}[-1] int 1 run scoreboard players get ${value.name} ${LOWERING_OBJ}`)
2153
- }
2154
- }
2155
- return { kind: 'const', value: 0 }
2156
- }
2157
-
2158
- if (expr.fn === '__array_pop') {
2159
- const arrName = this.getArrayStorageName(expr.args[0])
2160
- const dst = this.builder.freshTemp()
2161
- if (arrName) {
2162
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get storage rs:heap ${arrName}[-1]`)
2163
- this.builder.emitRaw(`data remove storage rs:heap ${arrName}[-1]`)
2164
- } else {
2165
- this.builder.emitAssign(dst, { kind: 'const', value: 0 })
2166
- }
2167
- return { kind: 'var', name: dst }
2168
- }
2169
-
2170
- // Handle spawn_object - creates world object (invisible armor stand)
2171
- if (expr.fn === 'spawn_object') {
2172
- const x = this.exprToString(expr.args[0])
2173
- const y = this.exprToString(expr.args[1])
2174
- const z = this.exprToString(expr.args[2])
2175
- const tag = `__rs_obj_${this.worldObjCounter++}`
2176
- this.builder.emitRaw(`summon minecraft:armor_stand ${x} ${y} ${z} {Invisible:1b,Marker:1b,NoGravity:1b,Tags:["${tag}"]}`)
2177
- // Return a selector pointing to this entity
2178
- const selector = `@e[tag=${tag},limit=1]`
2179
- return { kind: 'var', name: selector }
2180
- }
2181
-
2182
- // Handle kill for world objects
2183
- if (expr.fn === 'kill' && expr.args.length === 1 && expr.args[0].kind === 'ident') {
2184
- const mapped = this.varMap.get(expr.args[0].name)
2185
- if (mapped && mapped.startsWith('@e[tag=__rs_obj_')) {
2186
- this.builder.emitRaw(`kill ${mapped}`)
2187
- return { kind: 'const', value: 0 }
2188
- }
2189
- }
2190
-
2191
- const callbackTarget = this.resolveFunctionRefByName(expr.fn)
2192
- if (callbackTarget) {
2193
- return this.emitDirectFunctionCall(callbackTarget, expr.args)
2194
- }
2195
-
2196
- const implMethod = this.resolveInstanceMethod(expr)
2197
- if (implMethod) {
2198
- // Copy struct fields from instance to 'self' storage before calling
2199
- const receiver = expr.args[0]
2200
- if (receiver?.kind === 'ident') {
2201
- const receiverType = this.inferExprType(receiver)
2202
- if (receiverType?.kind === 'struct') {
2203
- const structDecl = this.structDecls.get(receiverType.name)
2204
- const structName = receiverType.name.toLowerCase()
2205
- if (structDecl) {
2206
- for (const field of structDecl.fields) {
2207
- const srcPath = `rs:heap ${structName}_${receiver.name}.${field.name}`
2208
- const dstPath = `rs:heap ${structName}_self.${field.name}`
2209
- this.builder.emitRaw(`data modify storage ${dstPath} set from storage ${srcPath}`)
2210
- }
2211
- }
2212
- }
2213
- }
2214
- return this.emitMethodCall(implMethod.loweredName, implMethod.fn, expr.args)
2215
- }
2216
-
2217
- // Regular function call
2218
- const fnDecl = this.fnDecls.get(expr.fn)
2219
- const defaultArgs = this.functionDefaults.get(expr.fn) ?? []
2220
- const fullArgs = [...expr.args]
2221
- for (let i = fullArgs.length; i < defaultArgs.length; i++) {
2222
- const defaultExpr = defaultArgs[i]
2223
- if (!defaultExpr) {
2224
- break
2225
- }
2226
- fullArgs.push(defaultExpr)
2227
- }
2228
-
2229
- if (fnDecl) {
2230
- const callbackBindings = new Map<string, string>()
2231
- const runtimeArgs: Expr[] = []
2232
-
2233
- for (let i = 0; i < fullArgs.length; i++) {
2234
- const param = fnDecl.params[i]
2235
- if (param && this.normalizeType(param.type).kind === 'function_type') {
2236
- const functionRef = this.resolveFunctionRefExpr(fullArgs[i])
2237
- if (!functionRef) {
2238
- throw new Error(`Cannot lower callback argument for parameter '${param.name}'`)
2239
- }
2240
- callbackBindings.set(param.name, functionRef)
2241
- continue
2242
- }
2243
- runtimeArgs.push(fullArgs[i])
2244
- }
2245
-
2246
- const stdlibCallSite = this.getStdlibCallSiteContext(fnDecl, getSpan(expr))
2247
- const targetFn = callbackBindings.size > 0 || stdlibCallSite
2248
- ? this.ensureSpecializedFunctionWithContext(fnDecl, callbackBindings, stdlibCallSite)
2249
- : expr.fn
2250
-
2251
- // Check if this is a call to a known macro function
2252
- const macroParams = this.macroFunctionInfo.get(targetFn)
2253
- if (macroParams && macroParams.length > 0) {
2254
- return this.emitMacroFunctionCall(targetFn, runtimeArgs, macroParams, fnDecl)
2255
- }
2256
-
2257
- return this.emitDirectFunctionCall(targetFn, runtimeArgs)
2258
- }
2259
-
2260
- // Check for macro function (forward-declared or external)
2261
- const macroParamsForUnknown = this.macroFunctionInfo.get(expr.fn)
2262
- if (macroParamsForUnknown && macroParamsForUnknown.length > 0) {
2263
- return this.emitMacroFunctionCall(expr.fn, fullArgs, macroParamsForUnknown, undefined)
2264
- }
2265
-
2266
- return this.emitDirectFunctionCall(expr.fn, fullArgs)
2267
- }
2268
-
2269
- private lowerStaticCallExpr(expr: Extract<Expr, { kind: 'static_call' }>): Operand {
2270
- const method = this.implMethods.get(expr.type)?.get(expr.method)
2271
- const targetFn = method?.loweredName ?? `${expr.type}_${expr.method}`
2272
- return this.emitMethodCall(targetFn, method?.fn, expr.args)
2273
- }
2274
-
2275
- private lowerInvokeExpr(expr: Extract<Expr, { kind: 'invoke' }>): Operand {
2276
- if (expr.callee.kind === 'lambda') {
2277
- if (!Array.isArray(expr.callee.body)) {
2278
- return this.inlineLambdaInvoke(expr.callee, expr.args)
2279
- }
2280
- const lambdaName = this.lowerLambdaExpr(expr.callee)
2281
- return this.emitDirectFunctionCall(lambdaName, expr.args)
2282
- }
2283
-
2284
- const functionRef = this.resolveFunctionRefExpr(expr.callee)
2285
- if (!functionRef) {
2286
- throw new Error('Cannot invoke a non-function value')
2287
- }
2288
- return this.emitDirectFunctionCall(functionRef, expr.args)
2289
- }
2290
-
2291
- private inlineLambdaInvoke(expr: Extract<Expr, { kind: 'lambda' }>, args: Expr[]): Operand {
2292
- const savedVarMap = new Map(this.varMap)
2293
- const savedVarTypes = new Map(this.varTypes)
2294
- const savedLambdaBindings = new Map(this.lambdaBindings)
2295
- const savedBlockPosVars = new Map(this.blockPosVars)
2296
-
2297
- for (let i = 0; i < expr.params.length; i++) {
2298
- const param = expr.params[i]
2299
- const temp = this.builder.freshTemp()
2300
- const arg = args[i]
2301
- this.builder.emitAssign(temp, arg ? this.lowerExpr(arg) : { kind: 'const', value: 0 })
2302
- this.varMap.set(param.name, temp)
2303
- if (param.type) {
2304
- this.varTypes.set(param.name, this.normalizeType(param.type))
2305
- }
2306
- this.lambdaBindings.delete(param.name)
2307
- this.blockPosVars.delete(param.name)
2308
- }
2309
-
2310
- const result = this.lowerExpr(expr.body as Expr)
2311
-
2312
- this.varMap = savedVarMap
2313
- this.varTypes = savedVarTypes
2314
- this.lambdaBindings = savedLambdaBindings
2315
- this.blockPosVars = savedBlockPosVars
2316
- return result
2317
- }
2318
-
2319
- private emitDirectFunctionCall(fn: string, args: Expr[]): Operand {
2320
- const loweredArgs: Operand[] = args.map(arg => this.lowerExpr(arg))
2321
- const dst = this.builder.freshTemp()
2322
- this.builder.emitCall(fn, loweredArgs, dst)
2323
- return { kind: 'var', name: dst }
2324
- }
2325
-
2326
- private emitMethodCall(fn: string, fnDecl: FnDecl | undefined, args: Expr[]): Operand {
2327
- const defaultArgs = this.functionDefaults.get(fn) ?? fnDecl?.params.map(param => param.default) ?? []
2328
- const fullArgs = [...args]
2329
- for (let i = fullArgs.length; i < defaultArgs.length; i++) {
2330
- const defaultExpr = defaultArgs[i]
2331
- if (!defaultExpr) {
2332
- break
2333
- }
2334
- fullArgs.push(defaultExpr)
2335
- }
2336
- return this.emitDirectFunctionCall(fn, fullArgs)
2337
- }
2338
-
2339
- private resolveFunctionRefExpr(expr: Expr): string | null {
2340
- if (expr.kind === 'lambda') {
2341
- return this.lowerLambdaExpr(expr)
2342
- }
2343
- if (expr.kind === 'ident') {
2344
- return this.resolveFunctionRefByName(expr.name) ?? (this.fnDecls.has(expr.name) ? expr.name : null)
2345
- }
2346
- return null
2347
- }
2348
-
2349
- private resolveFunctionRefByName(name: string): string | null {
2350
- return this.lambdaBindings.get(name) ?? this.currentCallbackBindings.get(name) ?? null
2351
- }
2352
-
2353
- private ensureSpecializedFunction(fn: FnDecl, callbackBindings: Map<string, string>): string {
2354
- return this.ensureSpecializedFunctionWithContext(fn, callbackBindings)
2355
- }
2356
-
2357
- private ensureSpecializedFunctionWithContext(
2358
- fn: FnDecl,
2359
- callbackBindings: Map<string, string>,
2360
- stdlibCallSite?: StdlibCallSiteContext
2361
- ): string {
2362
- const parts = [...callbackBindings.entries()]
2363
- .sort(([left], [right]) => left.localeCompare(right))
2364
- .map(([param, target]) => `${param}_${target.replace(/[^a-zA-Z0-9_]/g, '_')}`)
2365
- const callSiteHash = stdlibCallSite ? this.shortHash(this.serializeCallSite(stdlibCallSite)) : null
2366
- if (callSiteHash) {
2367
- parts.push(`callsite_${callSiteHash}`)
2368
- }
2369
- const key = `${fn.name}::${parts.join('::')}`
2370
- const cached = this.specializedFunctions.get(key)
2371
- if (cached) {
2372
- return cached
2373
- }
2374
-
2375
- const specializedName = `${fn.name}__${parts.join('__')}`
2376
- this.specializedFunctions.set(key, specializedName)
2377
- this.withSavedFunctionState(() => {
2378
- this.lowerFn(fn, { name: specializedName, callbackBindings, stdlibCallSite })
2379
- })
2380
- return specializedName
2381
- }
2382
-
2383
- private lowerLambdaExpr(expr: Extract<Expr, { kind: 'lambda' }>): string {
2384
- const lambdaName = `__lambda_${this.lambdaCounter++}`
2385
- const lambdaFn: FnDecl = {
2386
- name: lambdaName,
2387
- params: expr.params.map(param => ({
2388
- name: param.name,
2389
- type: param.type ?? { kind: 'named', name: 'int' },
2390
- })),
2391
- returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
2392
- decorators: [],
2393
- body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
2394
- }
2395
- this.withSavedFunctionState(() => {
2396
- this.lowerFn(lambdaFn)
2397
- })
2398
- return lambdaName
2399
- }
2400
-
2401
- private withSavedFunctionState<T>(callback: () => T): T {
2402
- const savedCurrentFn = this.currentFn
2403
- const savedStdlibCallSite = this.currentStdlibCallSite
2404
- const savedForeachCounter = this.foreachCounter
2405
- const savedBuilder = this.builder
2406
- const savedVarMap = new Map(this.varMap)
2407
- const savedLambdaBindings = new Map(this.lambdaBindings)
2408
- const savedIntervalBindings = new Map(this.intervalBindings)
2409
- const savedCallbackBindings = new Map(this.currentCallbackBindings)
2410
- const savedContext = this.currentContext
2411
- const savedBlockPosVars = new Map(this.blockPosVars)
2412
- const savedStringValues = new Map(this.stringValues)
2413
- const savedVarTypes = new Map(this.varTypes)
2414
- // Macro tracking state
2415
- const savedCurrentFnParamNames = new Set(this.currentFnParamNames)
2416
- const savedCurrentFnMacroParams = new Set(this.currentFnMacroParams)
2417
-
2418
- try {
2419
- return callback()
2420
- } finally {
2421
- this.currentFn = savedCurrentFn
2422
- this.currentStdlibCallSite = savedStdlibCallSite
2423
- this.foreachCounter = savedForeachCounter
2424
- this.builder = savedBuilder
2425
- this.varMap = savedVarMap
2426
- this.lambdaBindings = savedLambdaBindings
2427
- this.intervalBindings = savedIntervalBindings
2428
- this.currentCallbackBindings = savedCallbackBindings
2429
- this.currentContext = savedContext
2430
- this.blockPosVars = savedBlockPosVars
2431
- this.stringValues = savedStringValues
2432
- this.varTypes = savedVarTypes
2433
- this.currentFnParamNames = savedCurrentFnParamNames
2434
- this.currentFnMacroParams = savedCurrentFnMacroParams
2435
- }
2436
- }
2437
-
2438
- private lowerBuiltinCall(name: string, args: Expr[], callSpan?: Span): Operand {
2439
- const richTextCommand = this.lowerRichTextBuiltin(name, args)
2440
- if (richTextCommand) {
2441
- this.builder.emitRaw(richTextCommand)
2442
- return { kind: 'const', value: 0 }
2443
- }
2444
-
2445
- if (name === 'setTimeout') {
2446
- return this.lowerSetTimeout(args)
2447
- }
2448
-
2449
- if (name === 'setInterval') {
2450
- return this.lowerSetInterval(args)
2451
- }
2452
-
2453
- if (name === 'clearInterval') {
2454
- return this.lowerClearInterval(args, callSpan)
2455
- }
2456
-
2457
- // Special case: random - legacy scoreboard RNG for pre-1.20.3 compatibility
2458
- if (name === 'random') {
2459
- const dst = this.builder.freshTemp()
2460
- const min = args[0] ? this.exprToLiteral(args[0]) : '0'
2461
- const max = args[1] ? this.exprToLiteral(args[1]) : '100'
2462
- this.builder.emitRaw(`scoreboard players random ${dst} ${LOWERING_OBJ} ${min} ${max}`)
2463
- return { kind: 'var', name: dst }
2464
- }
2465
-
2466
- // Special case: random_native - /random value (MC 1.20.3+)
2467
- if (name === 'random_native') {
2468
- const dst = this.builder.freshTemp()
2469
- const min = args[0] ? this.exprToLiteral(args[0]) : '0'
2470
- const max = args[1] ? this.exprToLiteral(args[1]) : '100'
2471
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run random value ${min} ${max}`)
2472
- return { kind: 'var', name: dst }
2473
- }
2474
-
2475
- // Special case: random_sequence - /random reset (MC 1.20.3+)
2476
- if (name === 'random_sequence') {
2477
- const sequence = this.exprToString(args[0])
2478
- const seed = args[1] ? this.exprToLiteral(args[1]) : '0'
2479
- this.builder.emitRaw(`random reset ${sequence} ${seed}`)
2480
- return { kind: 'const', value: 0 }
2481
- }
2482
-
2483
- // Special case: scoreboard_get / score — read from vanilla MC scoreboard
2484
- if (name === 'scoreboard_get' || name === 'score') {
2485
- const dst = this.builder.freshTemp()
2486
- const player = this.exprToTargetString(args[0])
2487
- const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
2488
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run scoreboard players get ${player} ${objective}`)
2489
- return { kind: 'var', name: dst }
2490
- }
2491
-
2492
- // Special case: scoreboard_set — write to vanilla MC scoreboard
2493
- if (name === 'scoreboard_set') {
2494
- const player = this.exprToTargetString(args[0])
2495
- const objective = this.resolveScoreboardObjective(args[0], args[1], callSpan)
2496
- const value = this.lowerExpr(args[2])
2497
- if (value.kind === 'const') {
2498
- this.builder.emitRaw(`scoreboard players set ${player} ${objective} ${value.value}`)
2499
- } else if (value.kind === 'var') {
2500
- // Read directly from the computed scoreboard temp. Routing through a fresh
2501
- // temp here breaks once optimization removes the apparently-dead assign.
2502
- this.builder.emitRaw(`execute store result score ${player} ${objective} run scoreboard players get ${value.name} ${LOWERING_OBJ}`)
2503
- }
2504
- return { kind: 'const', value: 0 }
2505
- }
2506
-
2507
- if (name === 'scoreboard_display') {
2508
- const slot = this.exprToString(args[0])
2509
- const objective = this.resolveScoreboardObjective(undefined, args[1], callSpan)
2510
- this.builder.emitRaw(`scoreboard objectives setdisplay ${slot} ${objective}`)
2511
- return { kind: 'const', value: 0 }
2512
- }
2513
-
2514
- if (name === 'scoreboard_hide') {
2515
- const slot = this.exprToString(args[0])
2516
- this.builder.emitRaw(`scoreboard objectives setdisplay ${slot}`)
2517
- return { kind: 'const', value: 0 }
2518
- }
2519
-
2520
- if (name === 'scoreboard_add_objective') {
2521
- const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
2522
- const criteria = this.exprToString(args[1])
2523
- const displayName = args[2] ? ` ${this.exprToQuotedString(args[2])}` : ''
2524
- this.builder.emitRaw(`scoreboard objectives add ${objective} ${criteria}${displayName}`)
2525
- return { kind: 'const', value: 0 }
2526
- }
2527
-
2528
- if (name === 'scoreboard_remove_objective') {
2529
- const objective = this.resolveScoreboardObjective(undefined, args[0], callSpan)
2530
- this.builder.emitRaw(`scoreboard objectives remove ${objective}`)
2531
- return { kind: 'const', value: 0 }
2532
- }
2533
-
2534
- if (name === 'bossbar_add') {
2535
- const id = this.exprToString(args[0])
2536
- const title = this.exprToTextComponent(args[1])
2537
- this.builder.emitRaw(`bossbar add ${id} ${title}`)
2538
- return { kind: 'const', value: 0 }
2539
- }
2540
-
2541
- if (name === 'bossbar_set_value') {
2542
- this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} value ${this.exprToString(args[1])}`)
2543
- return { kind: 'const', value: 0 }
2544
- }
2545
-
2546
- if (name === 'bossbar_set_max') {
2547
- this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} max ${this.exprToString(args[1])}`)
2548
- return { kind: 'const', value: 0 }
2549
- }
2550
-
2551
- if (name === 'bossbar_set_color') {
2552
- this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} color ${this.exprToString(args[1])}`)
2553
- return { kind: 'const', value: 0 }
2554
- }
2555
-
2556
- if (name === 'bossbar_set_style') {
2557
- this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} style ${this.exprToString(args[1])}`)
2558
- return { kind: 'const', value: 0 }
2559
- }
2560
-
2561
- if (name === 'bossbar_set_visible') {
2562
- this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} visible ${this.exprToBoolString(args[1])}`)
2563
- return { kind: 'const', value: 0 }
2564
- }
2565
-
2566
- if (name === 'bossbar_set_players') {
2567
- this.builder.emitRaw(`bossbar set ${this.exprToString(args[0])} players ${this.exprToTargetString(args[1])}`)
2568
- return { kind: 'const', value: 0 }
2569
- }
2570
-
2571
- if (name === 'bossbar_remove') {
2572
- this.builder.emitRaw(`bossbar remove ${this.exprToString(args[0])}`)
2573
- return { kind: 'const', value: 0 }
2574
- }
2575
-
2576
- if (name === 'bossbar_get_value') {
2577
- const dst = this.builder.freshTemp()
2578
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run bossbar get ${this.exprToString(args[0])} value`)
2579
- return { kind: 'var', name: dst }
2580
- }
2581
-
2582
- if (name === 'team_add') {
2583
- const team = this.exprToString(args[0])
2584
- const displayName = args[1] ? ` ${this.exprToTextComponent(args[1])}` : ''
2585
- this.builder.emitRaw(`team add ${team}${displayName}`)
2586
- return { kind: 'const', value: 0 }
2587
- }
2588
-
2589
- if (name === 'team_remove') {
2590
- this.builder.emitRaw(`team remove ${this.exprToString(args[0])}`)
2591
- return { kind: 'const', value: 0 }
2592
- }
2593
-
2594
- if (name === 'team_join') {
2595
- this.builder.emitRaw(`team join ${this.exprToString(args[0])} ${this.exprToTargetString(args[1])}`)
2596
- return { kind: 'const', value: 0 }
2597
- }
2598
-
2599
- if (name === 'team_leave') {
2600
- this.builder.emitRaw(`team leave ${this.exprToTargetString(args[0])}`)
2601
- return { kind: 'const', value: 0 }
2602
- }
2603
-
2604
- if (name === 'team_option') {
2605
- const team = this.exprToString(args[0])
2606
- const option = this.exprToString(args[1])
2607
- const value = this.isTeamTextOption(option)
2608
- ? this.exprToTextComponent(args[2])
2609
- : this.exprToString(args[2])
2610
- this.builder.emitRaw(`team modify ${team} ${option} ${value}`)
2611
- return { kind: 'const', value: 0 }
2612
- }
2613
-
2614
- // Special case: data_get — read NBT data into a variable
2615
- // data_get(target_type, target, path, scale?)
2616
- // target_type: "entity", "block", "storage"
2617
- if (name === 'data_get') {
2618
- const dst = this.builder.freshTemp()
2619
- const targetType = this.exprToString(args[0])
2620
- const target = targetType === 'entity'
2621
- ? this.exprToTargetString(args[1])
2622
- : this.exprToString(args[1])
2623
- const path = this.exprToString(args[2])
2624
- const scale = args[3] ? this.exprToString(args[3]) : '1'
2625
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get ${targetType} ${target} ${path} ${scale}`)
2626
- return { kind: 'var', name: dst }
2627
- }
2628
-
2629
- // storage_get_int(storage_ns, array_key, index) -> int
2630
- // Reads one element from an NBT int-array stored in data storage.
2631
- // storage_ns : e.g. "math:tables"
2632
- // array_key : e.g. "sin"
2633
- // index : integer index (const or runtime)
2634
- //
2635
- // Const index: execute store result score $dst ${LOWERING_OBJ} run data get storage math:tables sin[N] 1
2636
- // Runtime index: macro sub-function via rs:heap, mirrors readArrayElement.
2637
- if (name === 'storage_get_int') {
2638
- const storageNs = this.exprToString(args[0]) // "math:tables"
2639
- const arrayKey = this.exprToString(args[1]) // "sin"
2640
- const indexOperand = this.lowerExpr(args[2])
2641
- const dst = this.builder.freshTemp()
2642
-
2643
- if (indexOperand.kind === 'const') {
2644
- this.builder.emitRaw(
2645
- `execute store result score ${dst} ${LOWERING_OBJ} run data get storage ${storageNs} ${arrayKey}[${indexOperand.value}] 1`
2646
- )
2647
- } else {
2648
- // Runtime index: store the index into rs:heap under a unique key,
2649
- // then call a macro sub-function that uses $(key) to index the array.
2650
- const macroKey = `__sgi_${this.foreachCounter++}`
2651
- const subFnName = `${this.currentFn}/__sgi_${this.foreachCounter++}`
2652
- const indexVar = indexOperand.kind === 'var'
2653
- ? indexOperand.name
2654
- : this.operandToVar(indexOperand)
2655
- this.builder.emitRaw(
2656
- `execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} ${LOWERING_OBJ}`
2657
- )
2658
- this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
2659
- // Prefix \x01 is a sentinel for the MC macro '$' line-start marker.
2660
- // We avoid using literal '$execute' here so the pre-alloc pass
2661
- // doesn't mistakenly register 'execute' as a scoreboard variable.
2662
- // Codegen replaces \x01 → '$' when emitting the mc function file.
2663
- this.emitRawSubFunction(
2664
- subFnName,
2665
- `\x01execute store result score ${dst} ${LOWERING_OBJ} run data get storage ${storageNs} ${arrayKey}[$(${macroKey})] 1`
2666
- )
2667
- }
2668
- return { kind: 'var', name: dst }
2669
- }
2670
-
2671
- // storage_set_array(storage_ns, array_key, nbt_array_literal)
2672
- // Writes a literal NBT int array to data storage (used in @load for tables).
2673
- // storage_set_array("math:tables", "sin", "[0, 17, 35, ...]")
2674
- if (name === 'storage_set_array') {
2675
- const storageNs = this.exprToString(args[0])
2676
- const arrayKey = this.exprToString(args[1])
2677
- const nbtLiteral = this.exprToString(args[2])
2678
- this.builder.emitRaw(
2679
- `data modify storage ${storageNs} ${arrayKey} set value ${nbtLiteral}`
2680
- )
2681
- return { kind: 'const', value: 0 }
2682
- }
2683
-
2684
- // storage_set_int(storage_ns, array_key, index, value) -> void
2685
- // Writes one integer element into an NBT int-array stored in data storage.
2686
- // storage_ns : e.g. "rs:bigint"
2687
- // array_key : e.g. "a"
2688
- // index : element index (const or runtime)
2689
- // value : integer value to write (const or runtime)
2690
- //
2691
- // Const index + const value:
2692
- // execute store result storage <ns> <key>[N] int 1 run scoreboard players set $const_V ${LOWERING_OBJ} V
2693
- // Runtime index or value: macro sub-function via rs:heap
2694
- if (name === 'storage_set_int') {
2695
- const storageNs = this.exprToString(args[0])
2696
- const arrayKey = this.exprToString(args[1])
2697
- const indexOperand = this.lowerExpr(args[2])
2698
- const valueOperand = this.lowerExpr(args[3])
2699
-
2700
- if (indexOperand.kind === 'const') {
2701
- // Static index — use execute store result to write to the fixed slot
2702
- const valVar = valueOperand.kind === 'var'
2703
- ? valueOperand.name
2704
- : this.operandToVar(valueOperand)
2705
- this.builder.emitRaw(
2706
- `execute store result storage ${storageNs} ${arrayKey}[${indexOperand.value}] int 1 run scoreboard players get ${valVar} ${LOWERING_OBJ}`
2707
- )
2708
- } else {
2709
- // Runtime index: we need a macro sub-function.
2710
- // Store index + value into rs:heap, call macro that does:
2711
- // $data modify storage <ns> <key>[$(idx_key)] set value $(val_key)
2712
- const macroIdxKey = `__ssi_i_${this.foreachCounter++}`
2713
- const macroValKey = `__ssi_v_${this.foreachCounter++}` // kept to pin valVar in optimizer
2714
- const subFnName = `${this.currentFn}/__ssi_${this.foreachCounter++}`
2715
- const indexVar = indexOperand.kind === 'var'
2716
- ? indexOperand.name
2717
- : this.operandToVar(indexOperand)
2718
- const valVar = valueOperand.kind === 'var'
2719
- ? valueOperand.name
2720
- : this.operandToVar(valueOperand)
2721
- this.builder.emitRaw(
2722
- `execute store result storage rs:heap ${macroIdxKey} int 1 run scoreboard players get ${indexVar} ${LOWERING_OBJ}`
2723
- )
2724
- // Pin valVar in the optimizer's read-set so the assignment is not dead-code-eliminated.
2725
- // The value is stored to rs:heap but NOT used by the macro (the macro reads the scoreboard
2726
- // slot directly to avoid the MC 'data modify set value $(n)' macro substitution bug).
2727
- this.builder.emitRaw(
2728
- `execute store result storage rs:heap ${macroValKey} int 1 run scoreboard players get ${valVar} ${LOWERING_OBJ}`
2729
- )
2730
- this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
2731
- // Use execute store result (not 'data modify set value $(val)') to avoid MC macro
2732
- // substitution bugs with numeric values. The scoreboard slot ${valVar} is hardcoded
2733
- // into the macro sub-function at compile time — only the array index is macro-substituted.
2734
- this.emitRawSubFunction(
2735
- subFnName,
2736
- `\x01execute store result storage ${storageNs} ${arrayKey}[$(${macroIdxKey})] int 1 run scoreboard players get ${valVar} ${LOWERING_OBJ}`
2737
- )
2738
- }
2739
- return { kind: 'const', value: 0 }
2740
- }
2741
-
2742
- // data_merge(target, nbt) — merge NBT into entity/block/storage
2743
- // data_merge(@s, { Invisible: 1b, Silent: 1b })
2744
- if (name === 'data_merge') {
2745
- const target = args[0]
2746
- const nbt = args[1]
2747
- const nbtStr = this.exprToSnbt ? this.exprToSnbt(nbt) : this.exprToString(nbt)
2748
-
2749
- // Check if target is a selector (entity) or string (block/storage)
2750
- if (target.kind === 'selector') {
2751
- const sel = this.exprToTargetString(target)
2752
- this.builder.emitRaw(`data merge entity ${sel} ${nbtStr}`)
2753
- } else {
2754
- // Assume block position or storage
2755
- const targetStr = this.exprToString(target)
2756
- // If it looks like coordinates, use block; otherwise storage
2757
- if (targetStr.match(/^~|^\d|^\^/)) {
2758
- this.builder.emitRaw(`data merge block ${targetStr} ${nbtStr}`)
2759
- } else {
2760
- this.builder.emitRaw(`data merge storage ${targetStr} ${nbtStr}`)
2761
- }
2762
- }
2763
- return { kind: 'const', value: 0 }
2764
- }
2765
-
2766
- // Set data structure operations — unique collections via NBT storage
2767
- // set_new is primarily handled in lowerLetStmt for proper string tracking.
2768
- // This fallback handles standalone set_new() calls without assignment.
2769
- if (name === 'set_new') {
2770
- const setId = `__set_${this.foreachCounter++}`
2771
- this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
2772
- return { kind: 'const', value: 0 }
2773
- }
2774
-
2775
- if (name === 'set_add') {
2776
- const setId = this.exprToString(args[0])
2777
- const value = this.exprToString(args[1])
2778
- this.builder.emitRaw(`execute unless data storage rs:sets ${setId}[{value:${value}}] run data modify storage rs:sets ${setId} append value {value:${value}}`)
2779
- return { kind: 'const', value: 0 }
2780
- }
2781
-
2782
- if (name === 'set_contains') {
2783
- const dst = this.builder.freshTemp()
2784
- const setId = this.exprToString(args[0])
2785
- const value = this.exprToString(args[1])
2786
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} if data storage rs:sets ${setId}[{value:${value}}]`)
2787
- return { kind: 'var', name: dst }
2788
- }
2789
-
2790
- if (name === 'set_remove') {
2791
- const setId = this.exprToString(args[0])
2792
- const value = this.exprToString(args[1])
2793
- this.builder.emitRaw(`data remove storage rs:sets ${setId}[{value:${value}}]`)
2794
- return { kind: 'const', value: 0 }
2795
- }
2796
-
2797
- if (name === 'set_clear') {
2798
- const setId = this.exprToString(args[0])
2799
- this.builder.emitRaw(`data modify storage rs:sets ${setId} set value []`)
2800
- return { kind: 'const', value: 0 }
2801
- }
2802
-
2803
- const coordCommand = this.lowerCoordinateBuiltin(name, args)
2804
- if (coordCommand) {
2805
- this.builder.emitRaw(coordCommand)
2806
- return { kind: 'const', value: 0 }
2807
- }
2808
-
2809
- if (name === 'tp_to') {
2810
- this.warnings.push({
2811
- message: 'tp_to is deprecated; use tp instead',
2812
- code: 'W_DEPRECATED',
2813
- ...(callSpan ? { line: callSpan.line, col: callSpan.col } : {}),
2814
- })
2815
- const tpResult = this.lowerTpCommandMacroAware(args)
2816
- if (tpResult) {
2817
- this.builder.emitRaw(tpResult.cmd)
2818
- }
2819
- return { kind: 'const', value: 0 }
2820
- }
2821
-
2822
- if (name === 'tp') {
2823
- const tpResult = this.lowerTpCommandMacroAware(args)
2824
- if (tpResult) {
2825
- this.builder.emitRaw(tpResult.cmd)
2826
- }
2827
- return { kind: 'const', value: 0 }
2828
- }
2829
-
2830
- // All builtins support macro params - check if any arg is a param needing macro treatment
2831
- const argResults = args.map(arg => this.exprToBuiltinArg(arg))
2832
- const hasMacroArg = argResults.some(r => r.macroParam !== undefined)
2833
- if (hasMacroArg) {
2834
- argResults.forEach(r => { if (r.macroParam) this.currentFnMacroParams.add(r.macroParam) })
2835
- }
2836
- const strArgs = argResults.map(r => r.str)
2837
- const cmd = BUILTINS[name]?.(strArgs)
2838
- if (cmd) {
2839
- this.builder.emitRaw(hasMacroArg ? `$${cmd}` : cmd)
2840
- }
2841
-
2842
- return { kind: 'const', value: 0 }
2843
- }
2844
-
2845
- private lowerSetTimeout(args: Expr[]): Operand {
2846
- const delay = this.exprToLiteral(args[0])
2847
- const callback = args[1]
2848
- if (!callback || callback.kind !== 'lambda') {
2849
- throw new DiagnosticError(
2850
- 'LoweringError',
2851
- 'setTimeout requires a lambda callback',
2852
- getSpan(callback) ?? { line: 1, col: 1 }
2853
- )
2854
- }
2855
-
2856
- const fnName = `__timeout_${this.timeoutCounter++}`
2857
- this.lowerNamedLambdaFunction(fnName, callback)
2858
- this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2859
- return { kind: 'const', value: 0 }
2860
- }
2861
-
2862
- private lowerSetInterval(args: Expr[]): Operand {
2863
- const delay = this.exprToLiteral(args[0])
2864
- const callback = args[1]
2865
- if (!callback || callback.kind !== 'lambda') {
2866
- throw new DiagnosticError(
2867
- 'LoweringError',
2868
- 'setInterval requires a lambda callback',
2869
- getSpan(callback) ?? { line: 1, col: 1 }
2870
- )
2871
- }
2872
-
2873
- const id = this.intervalCounter++
2874
- const bodyName = `__interval_body_${id}`
2875
- const fnName = `__interval_${id}`
2876
-
2877
- this.lowerNamedLambdaFunction(bodyName, callback)
2878
- this.lowerIntervalWrapperFunction(fnName, bodyName, delay)
2879
- this.intervalFunctions.set(id, fnName)
2880
- this.builder.emitRaw(`schedule function ${this.namespace}:${fnName} ${delay}t`)
2881
-
2882
- return { kind: 'const', value: id }
2883
- }
2884
-
2885
- private lowerClearInterval(args: Expr[], callSpan?: Span): Operand {
2886
- const fnName = this.resolveIntervalFunctionName(args[0])
2887
- if (!fnName) {
2888
- throw new DiagnosticError(
2889
- 'LoweringError',
2890
- 'clearInterval requires an interval ID returned from setInterval',
2891
- callSpan ?? getSpan(args[0]) ?? { line: 1, col: 1 }
2892
- )
2893
- }
2894
-
2895
- this.builder.emitRaw(`schedule clear ${this.namespace}:${fnName}`)
2896
- return { kind: 'const', value: 0 }
2897
- }
2898
-
2899
- private lowerNamedLambdaFunction(name: string, expr: Extract<Expr, { kind: 'lambda' }>): void {
2900
- const lambdaFn: FnDecl = {
2901
- name,
2902
- params: expr.params.map(param => ({
2903
- name: param.name,
2904
- type: param.type ?? { kind: 'named', name: 'int' },
2905
- })),
2906
- returnType: expr.returnType ?? this.inferLambdaReturnType(expr),
2907
- decorators: [],
2908
- body: Array.isArray(expr.body) ? expr.body : [{ kind: 'return', value: expr.body }],
2909
- }
2910
-
2911
- this.withSavedFunctionState(() => {
2912
- this.lowerFn(lambdaFn)
2913
- })
2914
- }
2915
-
2916
- private lowerIntervalWrapperFunction(name: string, bodyName: string, delay: string): void {
2917
- const intervalFn: FnDecl = {
2918
- name,
2919
- params: [],
2920
- returnType: { kind: 'named', name: 'void' },
2921
- decorators: [],
2922
- body: [
2923
- { kind: 'raw', cmd: `function ${this.namespace}:${bodyName}` },
2924
- { kind: 'raw', cmd: `schedule function ${this.namespace}:${name} ${delay}t` },
2925
- ],
2926
- }
2927
-
2928
- this.withSavedFunctionState(() => {
2929
- this.lowerFn(intervalFn)
2930
- })
2931
- }
2932
-
2933
- private resolveIntervalFunctionName(expr: Expr | undefined): string | null {
2934
- if (!expr) {
2935
- return null
2936
- }
2937
-
2938
- if (expr.kind === 'ident') {
2939
- const boundInterval = this.intervalBindings.get(expr.name)
2940
- if (boundInterval) {
2941
- return boundInterval
2942
- }
2943
-
2944
- const constValue = this.constValues.get(expr.name)
2945
- if (constValue?.kind === 'int_lit') {
2946
- return this.intervalFunctions.get(constValue.value) ?? null
2947
- }
2948
- return null
2949
- }
2950
-
2951
- if (expr.kind === 'int_lit') {
2952
- return this.intervalFunctions.get(expr.value) ?? null
2953
- }
2954
-
2955
- return null
2956
- }
2957
-
2958
- private lowerRichTextBuiltin(name: string, args: Expr[]): string | null {
2959
- const messageArgIndex = this.getRichTextArgIndex(name)
2960
- if (messageArgIndex === null) {
2961
- return null
2962
- }
2963
-
2964
- const messageExpr = args[messageArgIndex]
2965
- if (!messageExpr || (messageExpr.kind !== 'str_interp' && messageExpr.kind !== 'f_string')) {
2966
- return null
2967
- }
2968
-
2969
- const json = this.buildRichTextJson(messageExpr)
2970
-
2971
- switch (name) {
2972
- case 'say':
2973
- case 'announce':
2974
- return `tellraw @a ${json}`
2975
- case 'tell':
2976
- case 'tellraw':
2977
- return `tellraw ${this.exprToString(args[0])} ${json}`
2978
- case 'title':
2979
- return `title ${this.exprToString(args[0])} title ${json}`
2980
- case 'actionbar':
2981
- return `title ${this.exprToString(args[0])} actionbar ${json}`
2982
- case 'subtitle':
2983
- return `title ${this.exprToString(args[0])} subtitle ${json}`
2984
- default:
2985
- return null
2986
- }
2987
- }
2988
-
2989
- private getRichTextArgIndex(name: string): number | null {
2990
- switch (name) {
2991
- case 'say':
2992
- case 'announce':
2993
- return 0
2994
- case 'tell':
2995
- case 'tellraw':
2996
- case 'title':
2997
- case 'actionbar':
2998
- case 'subtitle':
2999
- return 1
3000
- default:
3001
- return null
3002
- }
3003
- }
3004
-
3005
- private buildRichTextJson(expr: Extract<Expr, { kind: 'str_interp' | 'f_string' }>): string {
3006
- const components: Array<string | Record<string, unknown>> = ['']
3007
-
3008
- if (expr.kind === 'f_string') {
3009
- for (const part of expr.parts) {
3010
- if (part.kind === 'text') {
3011
- if (part.value.length > 0) {
3012
- components.push({ text: part.value })
3013
- }
3014
- continue
3015
- }
3016
- this.appendRichTextExpr(components, part.expr)
3017
- }
3018
- return JSON.stringify(components)
3019
- }
3020
-
3021
- for (const part of expr.parts) {
3022
- if (typeof part === 'string') {
3023
- if (part.length > 0) {
3024
- components.push({ text: part })
3025
- }
3026
- continue
3027
- }
3028
-
3029
- this.appendRichTextExpr(components, part)
3030
- }
3031
-
3032
- return JSON.stringify(components)
3033
- }
3034
-
3035
- private appendRichTextExpr(components: Array<string | Record<string, unknown>>, expr: Expr): void {
3036
- if (expr.kind === 'ident') {
3037
- const constValue = this.constValues.get(expr.name)
3038
- if (constValue) {
3039
- this.appendRichTextExpr(components, constValue)
3040
- return
3041
- }
3042
- const stringValue = this.stringValues.get(expr.name)
3043
- if (stringValue !== undefined) {
3044
- components.push({ text: stringValue })
3045
- return
3046
- }
3047
- }
3048
-
3049
- if (expr.kind === 'str_lit') {
3050
- if (expr.value.length > 0) {
3051
- components.push({ text: expr.value })
3052
- }
3053
- return
3054
- }
3055
-
3056
- if (expr.kind === 'str_interp') {
3057
- for (const part of expr.parts) {
3058
- if (typeof part === 'string') {
3059
- if (part.length > 0) {
3060
- components.push({ text: part })
3061
- }
3062
- } else {
3063
- this.appendRichTextExpr(components, part)
3064
- }
3065
- }
3066
- return
3067
- }
3068
-
3069
- if (expr.kind === 'f_string') {
3070
- for (const part of expr.parts) {
3071
- if (part.kind === 'text') {
3072
- if (part.value.length > 0) {
3073
- components.push({ text: part.value })
3074
- }
3075
- } else {
3076
- this.appendRichTextExpr(components, part.expr)
3077
- }
3078
- }
3079
- return
3080
- }
3081
-
3082
- if (expr.kind === 'bool_lit') {
3083
- components.push({ text: expr.value ? 'true' : 'false' })
3084
- return
3085
- }
3086
-
3087
- if (expr.kind === 'int_lit') {
3088
- components.push({ text: expr.value.toString() })
3089
- return
3090
- }
3091
-
3092
- if (expr.kind === 'float_lit') {
3093
- components.push({ text: expr.value.toString() })
3094
- return
3095
- }
3096
-
3097
- const operand = this.lowerExpr(expr)
3098
- if (operand.kind === 'const') {
3099
- components.push({ text: operand.value.toString() })
3100
- return
3101
- }
3102
-
3103
- components.push({ score: { name: this.operandToVar(operand), objective: LOWERING_OBJ } })
3104
- }
3105
-
3106
- private exprToString(expr: Expr): string {
3107
- switch (expr.kind) {
3108
- case 'int_lit':
3109
- return expr.value.toString()
3110
- case 'float_lit':
3111
- return Math.trunc(expr.value).toString()
3112
- case 'byte_lit':
3113
- return `${expr.value}b`
3114
- case 'short_lit':
3115
- return `${expr.value}s`
3116
- case 'long_lit':
3117
- return `${expr.value}L`
3118
- case 'double_lit':
3119
- return `${expr.value}d`
3120
- case 'rel_coord':
3121
- return expr.value // ~ or ~5 or ~-3 - output as-is for MC commands
3122
- case 'local_coord':
3123
- return expr.value // ^ or ^5 or ^-3 - output as-is for MC commands
3124
- case 'bool_lit':
3125
- return expr.value ? '1' : '0'
3126
- case 'str_lit':
3127
- return expr.value
3128
- case 'mc_name':
3129
- return expr.value // #health → "health" (no quotes, used as bare MC name)
3130
- case 'str_interp':
3131
- case 'f_string':
3132
- return this.buildRichTextJson(expr)
3133
- case 'blockpos':
3134
- return emitBlockPos(expr)
3135
- case 'ident': {
3136
- const constValue = this.constValues.get(expr.name)
3137
- if (constValue) {
3138
- return this.exprToString(constValue)
3139
- }
3140
- const stringValue = this.stringValues.get(expr.name)
3141
- if (stringValue !== undefined) {
3142
- return stringValue
3143
- }
3144
- const mapped = this.varMap.get(expr.name)
3145
- return mapped ?? `$${expr.name}`
3146
- }
3147
- case 'selector':
3148
- return this.selectorToString(expr.sel)
3149
- case 'unary':
3150
- // Handle unary minus on literals directly
3151
- if (expr.op === '-' && expr.operand.kind === 'int_lit') {
3152
- return (-expr.operand.value).toString()
3153
- }
3154
- if (expr.op === '-' && expr.operand.kind === 'float_lit') {
3155
- return Math.trunc(-expr.operand.value).toString()
3156
- }
3157
- // Fall through to default for complex cases
3158
- const unaryOp = this.lowerExpr(expr)
3159
- return this.operandToVar(unaryOp)
3160
- default:
3161
- // Complex expression - lower and return var name
3162
- const op = this.lowerExpr(expr)
3163
- return this.operandToVar(op)
3164
- }
3165
- }
3166
-
3167
- private exprToEntitySelector(expr: Expr): string | null {
3168
- if (expr.kind === 'selector') {
3169
- return this.selectorToString(expr.sel)
3170
- }
3171
-
3172
- if (expr.kind === 'ident') {
3173
- const constValue = this.constValues.get(expr.name)
3174
- if (constValue) {
3175
- return this.exprToEntitySelector(constValue)
3176
- }
3177
- const mapped = this.varMap.get(expr.name)
3178
- if (mapped?.startsWith('@')) {
3179
- return mapped
3180
- }
3181
- }
3182
-
3183
- return null
3184
- }
3185
-
3186
- private appendTypeFilter(selector: string, mcType: string): string {
3187
- if (selector.endsWith(']')) {
3188
- return `${selector.slice(0, -1)},type=${mcType}]`
3189
- }
3190
- return `${selector}[type=${mcType}]`
3191
- }
3192
-
3193
- private exprToSnbt(expr: Expr): string {
3194
- switch (expr.kind) {
3195
- case 'struct_lit': {
3196
- const entries = expr.fields.map(f => `${f.name}:${this.exprToSnbt(f.value)}`)
3197
- return `{${entries.join(',')}}`
3198
- }
3199
- case 'array_lit': {
3200
- const items = expr.elements.map(e => this.exprToSnbt(e))
3201
- return `[${items.join(',')}]`
3202
- }
3203
- case 'str_lit':
3204
- return `"${expr.value}"`
3205
- case 'int_lit':
3206
- return String(expr.value)
3207
- case 'float_lit':
3208
- return String(expr.value)
3209
- case 'byte_lit':
3210
- return `${expr.value}b`
3211
- case 'short_lit':
3212
- return `${expr.value}s`
3213
- case 'long_lit':
3214
- return `${expr.value}L`
3215
- case 'double_lit':
3216
- return `${expr.value}d`
3217
- case 'bool_lit':
3218
- return expr.value ? '1b' : '0b'
3219
- default:
3220
- return this.exprToString(expr)
3221
- }
3222
- }
3223
-
3224
- private exprToTargetString(expr: Expr): string {
3225
- if (expr.kind === 'selector') {
3226
- return this.selectorToString(expr.sel)
3227
- }
3228
-
3229
- if (expr.kind === 'str_lit' && expr.value.startsWith('@')) {
3230
- const span = getSpan(expr)
3231
- this.warnings.push({
3232
- message: `Quoted selector "${expr.value}" is deprecated; pass ${expr.value} without quotes`,
3233
- code: 'W_QUOTED_SELECTOR',
3234
- ...(span ? { line: span.line, col: span.col } : {}),
3235
- })
3236
- return expr.value
3237
- }
3238
-
3239
- return this.exprToString(expr)
3240
- }
3241
-
3242
- private exprToLiteral(expr: Expr): string {
3243
- if (expr.kind === 'int_lit') return expr.value.toString()
3244
- if (expr.kind === 'float_lit') return Math.trunc(expr.value).toString()
3245
- return '0'
3246
- }
3247
-
3248
- private exprToQuotedString(expr: Expr): string {
3249
- return JSON.stringify(this.exprToString(expr))
3250
- }
3251
-
3252
- private exprToTextComponent(expr: Expr): string {
3253
- return JSON.stringify({ text: this.exprToString(expr) })
3254
- }
3255
-
3256
- private exprToBoolString(expr: Expr): string {
3257
- if (expr.kind === 'bool_lit') {
3258
- return expr.value ? 'true' : 'false'
3259
- }
3260
- return this.exprToString(expr)
3261
- }
3262
-
3263
- private isTeamTextOption(option: string): boolean {
3264
- return option === 'displayName' || option === 'prefix' || option === 'suffix'
3265
- }
3266
-
3267
- private exprToScoreboardObjective(expr: Expr, span?: Span): string {
3268
- if (expr.kind === 'mc_name') {
3269
- // 'rs' is the canonical token for the current RS scoreboard objective.
3270
- // Resolve to LOWERING_OBJ so it respects --scoreboard / namespace default.
3271
- return expr.value === 'rs' ? LOWERING_OBJ : expr.value
3272
- }
3273
-
3274
- const objective = this.exprToString(expr)
3275
- if (objective.startsWith('#') || objective.includes('.')) {
3276
- if (objective.startsWith('#')) {
3277
- const name = objective.slice(1)
3278
- // '#rs' is the canonical way to reference the current RS scoreboard objective.
3279
- // Resolve to LOWERING_OBJ so it tracks the --scoreboard option.
3280
- return name === 'rs' ? LOWERING_OBJ : name
3281
- }
3282
- return objective
3283
- }
3284
-
3285
- return `${this.getObjectiveNamespace(span)}.${objective}`
3286
- }
3287
-
3288
- private resolveScoreboardObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string {
3289
- const stdlibInternalObjective = this.tryGetStdlibInternalObjective(playerExpr, objectiveExpr, span)
3290
- if (stdlibInternalObjective) {
3291
- return stdlibInternalObjective
3292
- }
3293
- return this.exprToScoreboardObjective(objectiveExpr, span)
3294
- }
3295
-
3296
- private getObjectiveNamespace(span?: Span): string {
3297
- const filePath = this.filePathForSpan(span)
3298
- if (!filePath) {
3299
- return this.namespace
3300
- }
3301
-
3302
- return this.isStdlibFile(filePath) ? LOWERING_OBJ : this.namespace
3303
- }
3304
-
3305
- private tryGetStdlibInternalObjective(playerExpr: Expr | undefined, objectiveExpr: Expr, span?: Span): string | null {
3306
- if (!span || !this.currentStdlibCallSite || objectiveExpr.kind !== 'mc_name' || objectiveExpr.value !== 'rs') {
3307
- return null
3308
- }
3309
-
3310
- const filePath = this.filePathForSpan(span)
3311
- if (!filePath || !this.isStdlibFile(filePath)) {
3312
- return null
3313
- }
3314
-
3315
- const resourceBase = this.getStdlibInternalResourceBase(playerExpr)
3316
- if (!resourceBase) {
3317
- return null
3318
- }
3319
-
3320
- const hash = this.shortHash(this.serializeCallSite(this.currentStdlibCallSite))
3321
- return `${LOWERING_OBJ}._${resourceBase}_${hash}`
3322
- }
3323
-
3324
- private getStdlibInternalResourceBase(playerExpr: Expr | undefined): string | null {
3325
- if (!playerExpr || playerExpr.kind !== 'str_lit') {
3326
- return null
3327
- }
3328
-
3329
- const match = playerExpr.value.match(/^([a-z0-9]+)_/)
3330
- return match?.[1] ?? null
3331
- }
3332
-
3333
- private getStdlibCallSiteContext(fn: FnDecl, exprSpan?: Span): StdlibCallSiteContext | undefined {
3334
- const fnFilePath = this.filePathForSpan(getSpan(fn))
3335
- if (!fnFilePath || !this.isStdlibFile(fnFilePath)) {
3336
- return undefined
3337
- }
3338
-
3339
- if (this.currentStdlibCallSite) {
3340
- return this.currentStdlibCallSite
3341
- }
3342
-
3343
- if (!exprSpan) {
3344
- return undefined
3345
- }
3346
-
3347
- return {
3348
- filePath: this.filePathForSpan(exprSpan),
3349
- line: exprSpan.line,
3350
- col: exprSpan.col,
3351
- }
3352
- }
3353
-
3354
- private serializeCallSite(callSite: StdlibCallSiteContext): string {
3355
- return `${callSite.filePath ?? '<memory>'}:${callSite.line}:${callSite.col}`
3356
- }
3357
-
3358
- private shortHash(input: string): string {
3359
- let hash = 2166136261
3360
- for (let i = 0; i < input.length; i++) {
3361
- hash ^= input.charCodeAt(i)
3362
- hash = Math.imul(hash, 16777619)
3363
- }
3364
- return (hash >>> 0).toString(16).padStart(8, '0').slice(0, 4)
3365
- }
3366
-
3367
- private isStdlibFile(filePath: string): boolean {
3368
- const normalized = path.normalize(filePath)
3369
- const stdlibSegment = `${path.sep}src${path.sep}stdlib${path.sep}`
3370
- return normalized.includes(stdlibSegment)
3371
- }
3372
-
3373
- private filePathForSpan(span?: Span): string | undefined {
3374
- if (!span) {
3375
- return undefined
3376
- }
3377
-
3378
- const line = span.line
3379
- return this.sourceRanges.find(range => line >= range.startLine && line <= range.endLine)?.filePath
3380
- }
3381
-
3382
- private lowerCoordinateBuiltin(name: string, args: Expr[]): string | null {
3383
- const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
3384
- const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
3385
- const pos2 = args[2] ? this.resolveBlockPosExpr(args[2]) : null
3386
-
3387
- if (name === 'setblock') {
3388
- if (args.length === 2 && pos0) {
3389
- return `setblock ${emitBlockPos(pos0)} ${this.exprToString(args[1])}`
3390
- }
3391
- return null
3392
- }
3393
-
3394
- if (name === 'fill') {
3395
- if (args.length === 3 && pos0 && pos1) {
3396
- return `fill ${emitBlockPos(pos0)} ${emitBlockPos(pos1)} ${this.exprToString(args[2])}`
3397
- }
3398
- return null
3399
- }
3400
-
3401
- if (name === 'clone') {
3402
- if (args.length === 3 && pos0 && pos1 && pos2) {
3403
- return `clone ${emitBlockPos(pos0)} ${emitBlockPos(pos1)} ${emitBlockPos(pos2)}`
3404
- }
3405
- return null
3406
- }
3407
-
3408
- if (name === 'summon') {
3409
- if (args.length >= 2 && pos1) {
3410
- const nbt = args[2] ? ` ${this.exprToString(args[2])}` : ''
3411
- return `summon ${this.exprToString(args[0])} ${emitBlockPos(pos1)}${nbt}`
3412
- }
3413
- return null
3414
- }
3415
-
3416
- return null
3417
- }
3418
-
3419
- private lowerTpCommand(args: Expr[]): string | null {
3420
- const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
3421
- const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
3422
-
3423
- if (args.length === 1 && pos0) {
3424
- return `tp ${emitBlockPos(pos0)}`
3425
- }
3426
-
3427
- if (args.length === 2) {
3428
- if (pos1) {
3429
- return `tp ${this.exprToString(args[0])} ${emitBlockPos(pos1)}`
3430
- }
3431
- return `tp ${this.exprToString(args[0])} ${this.exprToString(args[1])}`
3432
- }
3433
-
3434
- if (args.length === 4) {
3435
- return `tp ${this.exprToString(args[0])} ${this.exprToString(args[1])} ${this.exprToString(args[2])} ${this.exprToString(args[3])}`
3436
- }
3437
-
3438
- return null
3439
- }
3440
-
3441
- private lowerTpCommandMacroAware(args: Expr[]): { cmd: string } | null {
3442
- const pos0 = args[0] ? this.resolveBlockPosExpr(args[0]) : null
3443
- const pos1 = args[1] ? this.resolveBlockPosExpr(args[1]) : null
3444
-
3445
- // If blockpos args are used, no macro needed (coords are already resolved)
3446
- if (args.length === 1 && pos0) {
3447
- return { cmd: `tp ${emitBlockPos(pos0)}` }
3448
- }
3449
- if (args.length === 2 && pos1) {
3450
- return { cmd: `tp ${this.exprToString(args[0])} ${emitBlockPos(pos1)}` }
3451
- }
3452
-
3453
- // Check for macro args (int params used as coordinates)
3454
- if (args.length >= 2) {
3455
- const argResults = args.map(a => this.exprToBuiltinArg(a))
3456
- const hasMacro = argResults.some(r => r.macroParam !== undefined)
3457
- if (hasMacro) {
3458
- argResults.forEach(r => { if (r.macroParam) this.currentFnMacroParams.add(r.macroParam) })
3459
- const strs = argResults.map(r => r.str)
3460
- if (args.length === 2) {
3461
- return { cmd: `$tp ${strs[0]} ${strs[1]}` }
3462
- }
3463
- if (args.length === 4) {
3464
- return { cmd: `$tp ${strs[0]} ${strs[1]} ${strs[2]} ${strs[3]}` }
3465
- }
3466
- }
3467
- }
3468
-
3469
- // Fallback to non-macro
3470
- const plain = this.lowerTpCommand(args)
3471
- return plain ? { cmd: plain } : null
3472
- }
3473
-
3474
- private resolveBlockPosExpr(expr: Expr): BlockPosExpr | null {
3475
- if (expr.kind === 'blockpos') {
3476
- return expr
3477
- }
3478
- if (expr.kind === 'ident') {
3479
- return this.blockPosVars.get(expr.name) ?? null
3480
- }
3481
- return null
3482
- }
3483
-
3484
- private getArrayStorageName(expr: Expr): string | null {
3485
- if (expr.kind === 'ident') {
3486
- return expr.name
3487
- }
3488
- return null
3489
- }
3490
-
3491
- private inferLambdaReturnType(expr: Extract<Expr, { kind: 'lambda' }>): TypeNode {
3492
- if (expr.returnType) {
3493
- return this.normalizeType(expr.returnType)
3494
- }
3495
- if (Array.isArray(expr.body)) {
3496
- return { kind: 'named', name: 'void' }
3497
- }
3498
- return this.inferExprType(expr.body) ?? { kind: 'named', name: 'void' }
3499
- }
3500
-
3501
- private inferExprType(expr: Expr): TypeNode | undefined {
3502
- if (expr.kind === 'int_lit') return { kind: 'named', name: 'int' }
3503
- if (expr.kind === 'float_lit') return { kind: 'named', name: 'float' }
3504
- if (expr.kind === 'bool_lit') return { kind: 'named', name: 'bool' }
3505
- if (expr.kind === 'str_lit' || expr.kind === 'str_interp') return { kind: 'named', name: 'string' }
3506
- if (expr.kind === 'f_string') return { kind: 'named', name: 'format_string' }
3507
- if (expr.kind === 'blockpos') return { kind: 'named', name: 'BlockPos' }
3508
- if (expr.kind === 'ident') {
3509
- const constValue = this.constValues.get(expr.name)
3510
- if (constValue) {
3511
- switch (constValue.kind) {
3512
- case 'int_lit':
3513
- return { kind: 'named', name: 'int' }
3514
- case 'float_lit':
3515
- return { kind: 'named', name: 'float' }
3516
- case 'bool_lit':
3517
- return { kind: 'named', name: 'bool' }
3518
- case 'str_lit':
3519
- return { kind: 'named', name: 'string' }
3520
- }
3521
- }
3522
- return this.varTypes.get(expr.name)
3523
- }
3524
- if (expr.kind === 'lambda') {
3525
- return {
3526
- kind: 'function_type',
3527
- params: expr.params.map(param => this.normalizeType(param.type ?? { kind: 'named', name: 'int' })),
3528
- return: this.inferLambdaReturnType(expr),
3529
- }
3530
- }
3531
- if (expr.kind === 'call') {
3532
- const resolved = this.resolveFunctionRefByName(expr.fn) ?? this.resolveInstanceMethod(expr)?.loweredName ?? expr.fn
3533
- return this.fnDecls.get(resolved)?.returnType
3534
- }
3535
- if (expr.kind === 'static_call') {
3536
- return this.implMethods.get(expr.type)?.get(expr.method)?.fn.returnType
3537
- }
3538
- if (expr.kind === 'invoke') {
3539
- const calleeType = this.inferExprType(expr.callee)
3540
- if (calleeType?.kind === 'function_type') {
3541
- return calleeType.return
3542
- }
3543
- }
3544
- if (expr.kind === 'binary') {
3545
- if (['==', '!=', '<', '<=', '>', '>=', '&&', '||'].includes(expr.op)) {
3546
- return { kind: 'named', name: 'bool' }
3547
- }
3548
- return this.inferExprType(expr.left)
3549
- }
3550
- if (expr.kind === 'unary') {
3551
- return expr.op === '!' ? { kind: 'named', name: 'bool' } : this.inferExprType(expr.operand)
3552
- }
3553
- if (expr.kind === 'array_lit') {
3554
- return {
3555
- kind: 'array',
3556
- elem: expr.elements[0] ? (this.inferExprType(expr.elements[0]) ?? { kind: 'named', name: 'int' }) : { kind: 'named', name: 'int' },
3557
- }
3558
- }
3559
- if (expr.kind === 'member' && expr.obj.kind === 'ident' && this.enumDefs.has(expr.obj.name)) {
3560
- return { kind: 'enum', name: expr.obj.name }
3561
- }
3562
- return undefined
3563
- }
3564
-
3565
- /**
3566
- * Checks a raw() command string for `${...}` interpolation containing runtime variables.
3567
- * - If the interpolated expression is a numeric literal → OK (MC macro syntax).
3568
- * - If the interpolated name is a compile-time constant (in constValues) → OK.
3569
- * - If the interpolated name is a known runtime variable (in varMap) → DiagnosticError.
3570
- * - Unknown names → OK (could be MC macro params or external constants).
3571
- *
3572
- * This catches the common mistake of writing raw("say ${score}") expecting interpolation,
3573
- * which would silently emit a literal `${score}` in the MC command.
3574
- */
3575
- private checkRawCommandInterpolation(cmd: string, span?: Span): void {
3576
- const interpRe = /\$\{([^}]+)\}/g
3577
- let match: RegExpExecArray | null
3578
- while ((match = interpRe.exec(cmd)) !== null) {
3579
- const name = match[1].trim()
3580
- // Numeric/boolean literals are fine (intentional MC macro syntax)
3581
- if (/^\d+(\.\d+)?$/.test(name) || name === 'true' || name === 'false') {
3582
- continue
3583
- }
3584
- // Compile-time constants are fine
3585
- if (this.constValues.has(name)) {
3586
- continue
3587
- }
3588
- // Only error if it's a known runtime variable (in varMap or function params)
3589
- // Unknown identifiers are left alone (could be MC macro params the user intends)
3590
- if (this.varMap.has(name) || this.currentFnParamNames.has(name)) {
3591
- const loc = span ?? { line: 1, col: 1 }
3592
- throw new DiagnosticError(
3593
- 'LoweringError',
3594
- `raw() command contains runtime variable interpolation '\${${name}}'. ` +
3595
- `Variables cannot be interpolated into raw commands at compile time. ` +
3596
- `Use f-string messages (say/tell/announce) or MC macro syntax '$(${name})' for MC 1.20.2+ commands.`,
3597
- loc
3598
- )
3599
- }
3600
- }
3601
- }
3602
-
3603
- private resolveInstanceMethod(expr: Extract<Expr, { kind: 'call' }>): { fn: FnDecl; loweredName: string } | null {
3604
- const receiver = expr.args[0]
3605
- if (!receiver) {
3606
- return null
3607
- }
3608
-
3609
- const receiverType = this.inferExprType(receiver)
3610
- if (receiverType?.kind !== 'struct') {
3611
- return null
3612
- }
3613
-
3614
- const method = this.implMethods.get(receiverType.name)?.get(expr.fn)
3615
- if (!method || method.fn.params[0]?.name !== 'self') {
3616
- return null
3617
- }
3618
-
3619
- return method
3620
- }
3621
-
3622
- private normalizeType(type: TypeNode): TypeNode {
3623
- if (type.kind === 'array') {
3624
- return { kind: 'array', elem: this.normalizeType(type.elem) }
3625
- }
3626
- if (type.kind === 'function_type') {
3627
- return {
3628
- kind: 'function_type',
3629
- params: type.params.map(param => this.normalizeType(param)),
3630
- return: this.normalizeType(type.return),
3631
- }
3632
- }
3633
- if ((type.kind === 'struct' || type.kind === 'enum') && this.enumDefs.has(type.name)) {
3634
- return { kind: 'enum', name: type.name }
3635
- }
3636
- return type
3637
- }
3638
-
3639
- private readArrayElement(arrayName: string, index: Operand): Operand {
3640
- const dst = this.builder.freshTemp()
3641
-
3642
- if (index.kind === 'const') {
3643
- this.builder.emitRaw(`execute store result score ${dst} ${LOWERING_OBJ} run data get storage rs:heap ${arrayName}[${index.value}]`)
3644
- return { kind: 'var', name: dst }
3645
- }
3646
-
3647
- const macroKey = `__rs_index_${this.foreachCounter++}`
3648
- const subFnName = `${this.currentFn}/array_get_${this.foreachCounter++}`
3649
- const indexVar = index.kind === 'var' ? index.name : this.operandToVar(index)
3650
- this.builder.emitRaw(`execute store result storage rs:heap ${macroKey} int 1 run scoreboard players get ${indexVar} ${LOWERING_OBJ}`)
3651
- this.builder.emitRaw(`function ${this.namespace}:${subFnName} with storage rs:heap`)
3652
- this.emitRawSubFunction(
3653
- subFnName,
3654
- `\x01execute store result score ${dst} ${LOWERING_OBJ} run data get storage rs:heap ${arrayName}[$(${macroKey})]`
3655
- )
3656
- return { kind: 'var', name: dst }
3657
- }
3658
-
3659
- private emitRawSubFunction(name: string, ...commands: string[]): void {
3660
- const builder = new LoweringBuilder()
3661
- builder.startBlock('entry')
3662
- for (const cmd of commands) {
3663
- builder.emitRaw(cmd)
3664
- }
3665
- builder.emitReturn()
3666
- this.functions.push(builder.build(name, [], false))
3667
- }
3668
-
3669
- // -------------------------------------------------------------------------
3670
- // Helpers
3671
- // -------------------------------------------------------------------------
3672
-
3673
- private storeStringValue(name: string, expr: Expr): boolean {
3674
- const value = this.resolveStaticString(expr)
3675
- if (value === null) {
3676
- this.stringValues.delete(name)
3677
- return false
3678
- }
3679
- this.stringValues.set(name, value)
3680
- this.builder.emitRaw(`data modify storage rs:strings ${name} set value ${JSON.stringify(value)}`)
3681
- return true
3682
- }
3683
-
3684
- private resolveStaticString(expr: Expr | undefined): string | null {
3685
- if (!expr) {
3686
- return null
3687
- }
3688
-
3689
- if (expr.kind === 'str_lit') {
3690
- return expr.value
3691
- }
3692
-
3693
- if (expr.kind === 'ident') {
3694
- const constValue = this.constValues.get(expr.name)
3695
- if (constValue?.kind === 'str_lit') {
3696
- return constValue.value
3697
- }
3698
- return this.stringValues.get(expr.name) ?? null
3699
- }
3700
-
3701
- return null
3702
- }
3703
-
3704
- private getStringStoragePath(expr: Expr | undefined): string | null {
3705
- if (!expr || expr.kind !== 'ident') {
3706
- return null
3707
- }
3708
-
3709
- if (this.stringValues.has(expr.name)) {
3710
- return `rs:strings ${expr.name}`
3711
- }
3712
-
3713
- return null
3714
- }
3715
-
3716
- private lowerConstLiteral(expr: ConstDecl['value']): Operand {
3717
- switch (expr.kind) {
3718
- case 'int_lit':
3719
- return { kind: 'const', value: expr.value }
3720
- case 'float_lit':
3721
- return { kind: 'const', value: Math.round(expr.value * 1000) }
3722
- case 'bool_lit':
3723
- return { kind: 'const', value: expr.value ? 1 : 0 }
3724
- case 'str_lit':
3725
- return { kind: 'const', value: 0 }
3726
- }
3727
- }
3728
-
3729
- private operandToVar(op: Operand): string {
3730
- if (op.kind === 'var') return op.name
3731
- // Constant needs to be stored in a temp
3732
- const dst = this.builder.freshTemp()
3733
- this.builder.emitAssign(dst, op)
3734
- return dst
3735
- }
3736
-
3737
- private selectorToString(sel: EntitySelector): string {
3738
- const { kind, filters } = sel
3739
- if (!filters) return this.finalizeSelector(kind)
3740
-
3741
- const parts: string[] = []
3742
- if (filters.type) parts.push(`type=${filters.type}`)
3743
- if (filters.distance) parts.push(`distance=${this.rangeToString(filters.distance)}`)
3744
- if (filters.tag) filters.tag.forEach(t => parts.push(`tag=${t}`))
3745
- if (filters.notTag) filters.notTag.forEach(t => parts.push(`tag=!${t}`))
3746
- if (filters.limit !== undefined) parts.push(`limit=${filters.limit}`)
3747
- if (filters.sort) parts.push(`sort=${filters.sort}`)
3748
- if (filters.scores) {
3749
- const scoreStr = Object.entries(filters.scores)
3750
- .map(([k, v]) => `${k}=${this.rangeToString(v)}`).join(',')
3751
- parts.push(`scores={${scoreStr}}`)
3752
- }
3753
- if (filters.nbt) parts.push(`nbt=${filters.nbt}`)
3754
- if (filters.gamemode) parts.push(`gamemode=${filters.gamemode}`)
3755
- // Position filters
3756
- if (filters.x) parts.push(`x=${this.rangeToString(filters.x)}`)
3757
- if (filters.y) parts.push(`y=${this.rangeToString(filters.y)}`)
3758
- if (filters.z) parts.push(`z=${this.rangeToString(filters.z)}`)
3759
- // Rotation filters
3760
- if (filters.x_rotation) parts.push(`x_rotation=${this.rangeToString(filters.x_rotation)}`)
3761
- if (filters.y_rotation) parts.push(`y_rotation=${this.rangeToString(filters.y_rotation)}`)
3762
-
3763
- return this.finalizeSelector(parts.length ? `${kind}[${parts.join(',')}]` : kind)
3764
- }
3765
-
3766
- private finalizeSelector(selector: string): string {
3767
- return normalizeSelector(selector, this.warnings)
3768
- }
3769
-
3770
- private rangeToString(r: RangeExpr): string {
3771
- if (r.min !== undefined && r.max !== undefined) {
3772
- if (r.min === r.max) return `${r.min}`
3773
- return `${r.min}..${r.max}`
3774
- }
3775
- if (r.min !== undefined) return `${r.min}..`
3776
- if (r.max !== undefined) return `..${r.max}`
3777
- return '..'
3778
- }
3779
- }
3780
-
3781
- // ---------------------------------------------------------------------------
3782
- // LoweringBuilder - Wrapper around IR construction
3783
- // ---------------------------------------------------------------------------
3784
-
3785
- class LoweringBuilder {
3786
- private static globalTempId = 0
3787
- private labelCount = 0
3788
- private blocks: any[] = []
3789
- private currentBlock: any = null
3790
- private locals = new Set<string>()
3791
-
3792
- /** Reset the global temp counter (call between compilations). */
3793
- static resetTempCounter(): void {
3794
- LoweringBuilder.globalTempId = 0
3795
- }
3796
-
3797
- freshTemp(): string {
3798
- const name = `$_${LoweringBuilder.globalTempId++}`
3799
- this.locals.add(name)
3800
- return name
3801
- }
3802
-
3803
- freshLabel(hint = 'L'): string {
3804
- return `${hint}_${this.labelCount++}`
3805
- }
3806
-
3807
- startBlock(label: string): void {
3808
- this.currentBlock = { label, instrs: [], term: null }
3809
- }
3810
-
3811
- isBlockSealed(): boolean {
3812
- return this.currentBlock === null || this.currentBlock.term !== null
3813
- }
3814
-
3815
- private sealBlock(term: any): void {
3816
- if (this.currentBlock) {
3817
- this.currentBlock.term = term
3818
- this.blocks.push(this.currentBlock)
3819
- this.currentBlock = null
3820
- }
3821
- }
3822
-
3823
- emitAssign(dst: string, src: Operand): void {
3824
- if (!dst.startsWith('$') && !dst.startsWith('@')) {
3825
- dst = '$' + dst
3826
- }
3827
- this.locals.add(dst)
3828
- this.currentBlock?.instrs.push({ op: 'assign', dst, src })
3829
- }
3830
-
3831
- emitBinop(dst: string, lhs: Operand, bop: BinOp, rhs: Operand): void {
3832
- this.locals.add(dst)
3833
- this.currentBlock?.instrs.push({ op: 'binop', dst, lhs, bop, rhs })
3834
- }
3835
-
3836
- emitCmp(dst: string, lhs: Operand, cop: CmpOp, rhs: Operand): void {
3837
- this.locals.add(dst)
3838
- this.currentBlock?.instrs.push({ op: 'cmp', dst, lhs, cop, rhs })
3839
- }
3840
-
3841
- emitCall(fn: string, args: Operand[], dst?: string): void {
3842
- if (dst) this.locals.add(dst)
3843
- this.currentBlock?.instrs.push({ op: 'call', fn, args, dst })
3844
- }
3845
-
3846
- emitRaw(cmd: string): void {
3847
- this.currentBlock?.instrs.push({ op: 'raw', cmd })
3848
- }
3849
-
3850
- emitJump(target: string): void {
3851
- this.sealBlock({ op: 'jump', target })
3852
- }
3853
-
3854
- emitJumpIf(cond: string, then: string, else_: string): void {
3855
- this.sealBlock({ op: 'jump_if', cond, then, else_ })
3856
- }
3857
-
3858
- emitReturn(value?: Operand): void {
3859
- this.sealBlock({ op: 'return', value })
3860
- }
3861
-
3862
- build(name: string, params: string[], isTickLoop = false): IRFunction {
3863
- // Ensure current block is sealed
3864
- if (this.currentBlock && !this.currentBlock.term) {
3865
- this.sealBlock({ op: 'return' })
3866
- }
3867
-
3868
- return {
3869
- name,
3870
- params,
3871
- locals: Array.from(this.locals),
3872
- blocks: this.blocks,
3873
- isTickLoop,
3874
- }
3875
- }
3876
- }