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