redscript-mc 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (272) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +40 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +31 -0
  3. package/.github/ISSUE_TEMPLATE/wrong_output.md +33 -0
  4. package/.github/PULL_REQUEST_TEMPLATE.md +34 -0
  5. package/.github/workflows/ci.yml +29 -0
  6. package/.github/workflows/publish-extension.yml +35 -0
  7. package/LICENSE +21 -0
  8. package/README.md +261 -0
  9. package/README.zh.md +261 -0
  10. package/dist/__tests__/cli.test.d.ts +1 -0
  11. package/dist/__tests__/cli.test.js +140 -0
  12. package/dist/__tests__/codegen.test.d.ts +1 -0
  13. package/dist/__tests__/codegen.test.js +121 -0
  14. package/dist/__tests__/diagnostics.test.d.ts +4 -0
  15. package/dist/__tests__/diagnostics.test.js +149 -0
  16. package/dist/__tests__/e2e.test.d.ts +6 -0
  17. package/dist/__tests__/e2e.test.js +1528 -0
  18. package/dist/__tests__/lexer.test.d.ts +1 -0
  19. package/dist/__tests__/lexer.test.js +316 -0
  20. package/dist/__tests__/lowering.test.d.ts +1 -0
  21. package/dist/__tests__/lowering.test.js +819 -0
  22. package/dist/__tests__/mc-integration.test.d.ts +12 -0
  23. package/dist/__tests__/mc-integration.test.js +395 -0
  24. package/dist/__tests__/mc-syntax.test.d.ts +1 -0
  25. package/dist/__tests__/mc-syntax.test.js +112 -0
  26. package/dist/__tests__/nbt.test.d.ts +1 -0
  27. package/dist/__tests__/nbt.test.js +82 -0
  28. package/dist/__tests__/optimizer-advanced.test.d.ts +1 -0
  29. package/dist/__tests__/optimizer-advanced.test.js +124 -0
  30. package/dist/__tests__/optimizer.test.d.ts +1 -0
  31. package/dist/__tests__/optimizer.test.js +118 -0
  32. package/dist/__tests__/parser.test.d.ts +1 -0
  33. package/dist/__tests__/parser.test.js +717 -0
  34. package/dist/__tests__/repl.test.d.ts +1 -0
  35. package/dist/__tests__/repl.test.js +27 -0
  36. package/dist/__tests__/runtime.test.d.ts +1 -0
  37. package/dist/__tests__/runtime.test.js +276 -0
  38. package/dist/__tests__/structure-optimizer.test.d.ts +1 -0
  39. package/dist/__tests__/structure-optimizer.test.js +33 -0
  40. package/dist/__tests__/typechecker.test.d.ts +1 -0
  41. package/dist/__tests__/typechecker.test.js +364 -0
  42. package/dist/ast/types.d.ts +357 -0
  43. package/dist/ast/types.js +9 -0
  44. package/dist/cli.d.ts +11 -0
  45. package/dist/cli.js +407 -0
  46. package/dist/codegen/cmdblock/index.d.ts +26 -0
  47. package/dist/codegen/cmdblock/index.js +45 -0
  48. package/dist/codegen/mcfunction/index.d.ts +34 -0
  49. package/dist/codegen/mcfunction/index.js +413 -0
  50. package/dist/codegen/structure/index.d.ts +18 -0
  51. package/dist/codegen/structure/index.js +249 -0
  52. package/dist/compile.d.ts +30 -0
  53. package/dist/compile.js +152 -0
  54. package/dist/data/arena/function/__load.mcfunction +6 -0
  55. package/dist/data/arena/function/__tick.mcfunction +2 -0
  56. package/dist/data/arena/function/announce_leaders/else_1.mcfunction +3 -0
  57. package/dist/data/arena/function/announce_leaders/foreach_0/merge_2.mcfunction +1 -0
  58. package/dist/data/arena/function/announce_leaders/foreach_0/then_0.mcfunction +3 -0
  59. package/dist/data/arena/function/announce_leaders/foreach_0.mcfunction +7 -0
  60. package/dist/data/arena/function/announce_leaders/foreach_1/merge_2.mcfunction +1 -0
  61. package/dist/data/arena/function/announce_leaders/foreach_1/then_0.mcfunction +4 -0
  62. package/dist/data/arena/function/announce_leaders/foreach_1.mcfunction +6 -0
  63. package/dist/data/arena/function/announce_leaders/merge_2.mcfunction +1 -0
  64. package/dist/data/arena/function/announce_leaders/then_0.mcfunction +4 -0
  65. package/dist/data/arena/function/announce_leaders.mcfunction +6 -0
  66. package/dist/data/arena/function/arena_tick/merge_2.mcfunction +1 -0
  67. package/dist/data/arena/function/arena_tick/then_0.mcfunction +4 -0
  68. package/dist/data/arena/function/arena_tick.mcfunction +11 -0
  69. package/dist/data/counter/function/__load.mcfunction +5 -0
  70. package/dist/data/counter/function/__tick.mcfunction +2 -0
  71. package/dist/data/counter/function/counter_tick/merge_2.mcfunction +1 -0
  72. package/dist/data/counter/function/counter_tick/then_0.mcfunction +3 -0
  73. package/dist/data/counter/function/counter_tick.mcfunction +11 -0
  74. package/dist/data/minecraft/tags/function/load.json +5 -0
  75. package/dist/data/minecraft/tags/function/tick.json +5 -0
  76. package/dist/data/quiz/function/__load.mcfunction +16 -0
  77. package/dist/data/quiz/function/__tick.mcfunction +6 -0
  78. package/dist/data/quiz/function/__trigger_quiz_a_dispatch.mcfunction +4 -0
  79. package/dist/data/quiz/function/__trigger_quiz_b_dispatch.mcfunction +4 -0
  80. package/dist/data/quiz/function/__trigger_quiz_c_dispatch.mcfunction +4 -0
  81. package/dist/data/quiz/function/__trigger_quiz_start_dispatch.mcfunction +4 -0
  82. package/dist/data/quiz/function/answer_a.mcfunction +4 -0
  83. package/dist/data/quiz/function/answer_b.mcfunction +4 -0
  84. package/dist/data/quiz/function/answer_c.mcfunction +4 -0
  85. package/dist/data/quiz/function/ask_question/else_1.mcfunction +5 -0
  86. package/dist/data/quiz/function/ask_question/else_4.mcfunction +5 -0
  87. package/dist/data/quiz/function/ask_question/else_7.mcfunction +4 -0
  88. package/dist/data/quiz/function/ask_question/merge_2.mcfunction +1 -0
  89. package/dist/data/quiz/function/ask_question/merge_5.mcfunction +2 -0
  90. package/dist/data/quiz/function/ask_question/merge_8.mcfunction +2 -0
  91. package/dist/data/quiz/function/ask_question/then_0.mcfunction +4 -0
  92. package/dist/data/quiz/function/ask_question/then_3.mcfunction +4 -0
  93. package/dist/data/quiz/function/ask_question/then_6.mcfunction +4 -0
  94. package/dist/data/quiz/function/ask_question.mcfunction +7 -0
  95. package/dist/data/quiz/function/finish_quiz.mcfunction +6 -0
  96. package/dist/data/quiz/function/handle_answer/else_1.mcfunction +5 -0
  97. package/dist/data/quiz/function/handle_answer/else_10.mcfunction +3 -0
  98. package/dist/data/quiz/function/handle_answer/else_16.mcfunction +3 -0
  99. package/dist/data/quiz/function/handle_answer/else_4.mcfunction +3 -0
  100. package/dist/data/quiz/function/handle_answer/else_7.mcfunction +5 -0
  101. package/dist/data/quiz/function/handle_answer/merge_11.mcfunction +2 -0
  102. package/dist/data/quiz/function/handle_answer/merge_14.mcfunction +2 -0
  103. package/dist/data/quiz/function/handle_answer/merge_17.mcfunction +2 -0
  104. package/dist/data/quiz/function/handle_answer/merge_2.mcfunction +8 -0
  105. package/dist/data/quiz/function/handle_answer/merge_5.mcfunction +2 -0
  106. package/dist/data/quiz/function/handle_answer/merge_8.mcfunction +2 -0
  107. package/dist/data/quiz/function/handle_answer/then_0.mcfunction +5 -0
  108. package/dist/data/quiz/function/handle_answer/then_12.mcfunction +5 -0
  109. package/dist/data/quiz/function/handle_answer/then_15.mcfunction +6 -0
  110. package/dist/data/quiz/function/handle_answer/then_3.mcfunction +6 -0
  111. package/dist/data/quiz/function/handle_answer/then_6.mcfunction +5 -0
  112. package/dist/data/quiz/function/handle_answer/then_9.mcfunction +6 -0
  113. package/dist/data/quiz/function/handle_answer.mcfunction +11 -0
  114. package/dist/data/quiz/function/start_quiz.mcfunction +5 -0
  115. package/dist/data/shop/function/__load.mcfunction +7 -0
  116. package/dist/data/shop/function/__tick.mcfunction +3 -0
  117. package/dist/data/shop/function/__trigger_shop_buy_dispatch.mcfunction +4 -0
  118. package/dist/data/shop/function/complete_purchase/else_1.mcfunction +5 -0
  119. package/dist/data/shop/function/complete_purchase/else_4.mcfunction +5 -0
  120. package/dist/data/shop/function/complete_purchase/else_7.mcfunction +3 -0
  121. package/dist/data/shop/function/complete_purchase/merge_2.mcfunction +2 -0
  122. package/dist/data/shop/function/complete_purchase/merge_5.mcfunction +2 -0
  123. package/dist/data/shop/function/complete_purchase/merge_8.mcfunction +2 -0
  124. package/dist/data/shop/function/complete_purchase/then_0.mcfunction +4 -0
  125. package/dist/data/shop/function/complete_purchase/then_3.mcfunction +4 -0
  126. package/dist/data/shop/function/complete_purchase/then_6.mcfunction +4 -0
  127. package/dist/data/shop/function/complete_purchase.mcfunction +7 -0
  128. package/dist/data/shop/function/handle_shop_trigger.mcfunction +3 -0
  129. package/dist/data/turret/function/__load.mcfunction +5 -0
  130. package/dist/data/turret/function/__tick.mcfunction +4 -0
  131. package/dist/data/turret/function/__trigger_deploy_turret_dispatch.mcfunction +4 -0
  132. package/dist/data/turret/function/deploy_turret.mcfunction +8 -0
  133. package/dist/data/turret/function/turret_tick/at_1.mcfunction +2 -0
  134. package/dist/data/turret/function/turret_tick/foreach_0.mcfunction +2 -0
  135. package/dist/data/turret/function/turret_tick/foreach_2.mcfunction +2 -0
  136. package/dist/data/turret/function/turret_tick/tick_body.mcfunction +3 -0
  137. package/dist/data/turret/function/turret_tick/tick_skip.mcfunction +1 -0
  138. package/dist/data/turret/function/turret_tick.mcfunction +5 -0
  139. package/dist/diagnostics/index.d.ts +44 -0
  140. package/dist/diagnostics/index.js +140 -0
  141. package/dist/index.d.ts +53 -0
  142. package/dist/index.js +126 -0
  143. package/dist/ir/builder.d.ts +32 -0
  144. package/dist/ir/builder.js +99 -0
  145. package/dist/ir/types.d.ts +117 -0
  146. package/dist/ir/types.js +15 -0
  147. package/dist/lexer/index.d.ts +36 -0
  148. package/dist/lexer/index.js +458 -0
  149. package/dist/lowering/index.d.ts +106 -0
  150. package/dist/lowering/index.js +2041 -0
  151. package/dist/mc-test/client.d.ts +128 -0
  152. package/dist/mc-test/client.js +174 -0
  153. package/dist/mc-test/runner.d.ts +28 -0
  154. package/dist/mc-test/runner.js +150 -0
  155. package/dist/mc-test/setup.d.ts +11 -0
  156. package/dist/mc-test/setup.js +98 -0
  157. package/dist/mc-validator/index.d.ts +17 -0
  158. package/dist/mc-validator/index.js +322 -0
  159. package/dist/nbt/index.d.ts +86 -0
  160. package/dist/nbt/index.js +250 -0
  161. package/dist/optimizer/commands.d.ts +36 -0
  162. package/dist/optimizer/commands.js +349 -0
  163. package/dist/optimizer/passes.d.ts +34 -0
  164. package/dist/optimizer/passes.js +227 -0
  165. package/dist/optimizer/structure.d.ts +8 -0
  166. package/dist/optimizer/structure.js +344 -0
  167. package/dist/pack.mcmeta +6 -0
  168. package/dist/parser/index.d.ts +76 -0
  169. package/dist/parser/index.js +1193 -0
  170. package/dist/repl.d.ts +16 -0
  171. package/dist/repl.js +165 -0
  172. package/dist/runtime/index.d.ts +101 -0
  173. package/dist/runtime/index.js +1288 -0
  174. package/dist/typechecker/index.d.ts +42 -0
  175. package/dist/typechecker/index.js +629 -0
  176. package/docs/COMPILATION_STATS.md +142 -0
  177. package/docs/IMPLEMENTATION_GUIDE.md +512 -0
  178. package/docs/LANGUAGE_REFERENCE.md +415 -0
  179. package/docs/MC_MAPPING.md +280 -0
  180. package/docs/STRUCTURE_TARGET.md +80 -0
  181. package/docs/mc-reference/commands.md +259 -0
  182. package/editors/vscode/.vscodeignore +10 -0
  183. package/editors/vscode/LICENSE +21 -0
  184. package/editors/vscode/README.md +78 -0
  185. package/editors/vscode/build.mjs +28 -0
  186. package/editors/vscode/icon.png +0 -0
  187. package/editors/vscode/mcfunction-language-configuration.json +28 -0
  188. package/editors/vscode/out/extension.js +7236 -0
  189. package/editors/vscode/package-lock.json +566 -0
  190. package/editors/vscode/package.json +137 -0
  191. package/editors/vscode/redscript-language-configuration.json +28 -0
  192. package/editors/vscode/snippets/redscript.json +114 -0
  193. package/editors/vscode/src/codeactions.ts +89 -0
  194. package/editors/vscode/src/completion.ts +130 -0
  195. package/editors/vscode/src/extension.ts +239 -0
  196. package/editors/vscode/src/hover.ts +1120 -0
  197. package/editors/vscode/src/symbols.ts +207 -0
  198. package/editors/vscode/syntaxes/mcfunction.tmLanguage.json +740 -0
  199. package/editors/vscode/syntaxes/redscript.tmLanguage.json +357 -0
  200. package/editors/vscode/tsconfig.json +13 -0
  201. package/jest.config.js +5 -0
  202. package/package.json +38 -0
  203. package/src/__tests__/cli.test.ts +130 -0
  204. package/src/__tests__/codegen.test.ts +128 -0
  205. package/src/__tests__/diagnostics.test.ts +195 -0
  206. package/src/__tests__/e2e.test.ts +1721 -0
  207. package/src/__tests__/fixtures/mc-commands-1.21.4.json +18734 -0
  208. package/src/__tests__/formatter.test.ts +46 -0
  209. package/src/__tests__/lexer.test.ts +356 -0
  210. package/src/__tests__/lowering.test.ts +962 -0
  211. package/src/__tests__/mc-integration.test.ts +409 -0
  212. package/src/__tests__/mc-syntax.test.ts +96 -0
  213. package/src/__tests__/nbt.test.ts +58 -0
  214. package/src/__tests__/optimizer-advanced.test.ts +144 -0
  215. package/src/__tests__/optimizer.test.ts +129 -0
  216. package/src/__tests__/parser.test.ts +800 -0
  217. package/src/__tests__/repl.test.ts +33 -0
  218. package/src/__tests__/runtime.test.ts +289 -0
  219. package/src/__tests__/structure-optimizer.test.ts +38 -0
  220. package/src/__tests__/typechecker.test.ts +395 -0
  221. package/src/ast/types.ts +248 -0
  222. package/src/cli.ts +445 -0
  223. package/src/codegen/cmdblock/index.ts +63 -0
  224. package/src/codegen/mcfunction/index.ts +471 -0
  225. package/src/codegen/structure/index.ts +305 -0
  226. package/src/compile.ts +188 -0
  227. package/src/diagnostics/index.ts +186 -0
  228. package/src/examples/README.md +77 -0
  229. package/src/examples/SHOWCASE_GAME.md +43 -0
  230. package/src/examples/arena.rs +44 -0
  231. package/src/examples/counter.rs +12 -0
  232. package/src/examples/pvp_arena.rs +131 -0
  233. package/src/examples/quiz.rs +90 -0
  234. package/src/examples/rpg.rs +13 -0
  235. package/src/examples/shop.rs +30 -0
  236. package/src/examples/showcase_game.rs +552 -0
  237. package/src/examples/stdlib_demo.rs +181 -0
  238. package/src/examples/turret.rs +27 -0
  239. package/src/examples/world_manager.rs +23 -0
  240. package/src/formatter/index.ts +22 -0
  241. package/src/index.ts +161 -0
  242. package/src/ir/builder.ts +114 -0
  243. package/src/ir/types.ts +119 -0
  244. package/src/lexer/index.ts +555 -0
  245. package/src/lowering/index.ts +2406 -0
  246. package/src/mc-test/client.ts +259 -0
  247. package/src/mc-test/runner.ts +140 -0
  248. package/src/mc-test/setup.ts +70 -0
  249. package/src/mc-validator/index.ts +367 -0
  250. package/src/nbt/index.ts +321 -0
  251. package/src/optimizer/commands.ts +416 -0
  252. package/src/optimizer/passes.ts +233 -0
  253. package/src/optimizer/structure.ts +441 -0
  254. package/src/parser/index.ts +1437 -0
  255. package/src/repl.ts +165 -0
  256. package/src/runtime/index.ts +1403 -0
  257. package/src/stdlib/README.md +156 -0
  258. package/src/stdlib/combat.rs +20 -0
  259. package/src/stdlib/cooldown.rs +45 -0
  260. package/src/stdlib/math.rs +49 -0
  261. package/src/stdlib/mobs.rs +99 -0
  262. package/src/stdlib/player.rs +29 -0
  263. package/src/stdlib/strings.rs +7 -0
  264. package/src/stdlib/timer.rs +51 -0
  265. package/src/templates/README.md +126 -0
  266. package/src/templates/combat.rs +96 -0
  267. package/src/templates/economy.rs +40 -0
  268. package/src/templates/mini-game-framework.rs +117 -0
  269. package/src/templates/quest.rs +78 -0
  270. package/src/test_programs/zombie_game.rs +25 -0
  271. package/src/typechecker/index.ts +737 -0
  272. package/tsconfig.json +16 -0
@@ -0,0 +1,962 @@
1
+ import { Lexer } from '../lexer'
2
+ import { Parser } from '../parser'
3
+ import { Lowering } from '../lowering'
4
+ import type { IRModule, IRFunction, IRInstr } from '../ir/types'
5
+
6
+ function compile(source: string, namespace = 'test'): IRModule {
7
+ return compileWithWarnings(source, namespace).ir
8
+ }
9
+
10
+ function compileWithWarnings(source: string, namespace = 'test'): { ir: IRModule; warnings: Lowering['warnings'] } {
11
+ const tokens = new Lexer(source).tokenize()
12
+ const ast = new Parser(tokens).parse(namespace)
13
+ const lowering = new Lowering(namespace)
14
+ return { ir: lowering.lower(ast), warnings: lowering.warnings }
15
+ }
16
+
17
+ function getFunction(module: IRModule, name: string): IRFunction | undefined {
18
+ return module.functions.find(f => f.name === name)
19
+ }
20
+
21
+ function getInstructions(fn: IRFunction): IRInstr[] {
22
+ return fn.blocks.flatMap(b => b.instrs)
23
+ }
24
+
25
+ function getRawCommands(fn: IRFunction): string[] {
26
+ return getInstructions(fn)
27
+ .filter((i): i is IRInstr & { op: 'raw' } => i.op === 'raw')
28
+ .map(i => i.cmd)
29
+ }
30
+
31
+ describe('Lowering', () => {
32
+ describe('basic functions', () => {
33
+ it('lowers empty function', () => {
34
+ const ir = compile('fn empty() {}')
35
+ const fn = getFunction(ir, 'empty')
36
+ expect(fn).toBeDefined()
37
+ expect(fn?.blocks).toHaveLength(1)
38
+ expect(fn?.blocks[0].term.op).toBe('return')
39
+ })
40
+
41
+ it('lowers function with params', () => {
42
+ const ir = compile('fn add(a: int, b: int) -> int { return a + b; }')
43
+ const fn = getFunction(ir, 'add')
44
+ expect(fn).toBeDefined()
45
+ expect(fn?.params).toEqual(['$a', '$b'])
46
+ })
47
+
48
+ it('creates param copy instructions', () => {
49
+ const ir = compile('fn foo(x: int) {}')
50
+ const fn = getFunction(ir, 'foo')!
51
+ const instrs = getInstructions(fn)
52
+ expect(instrs.some(i =>
53
+ i.op === 'assign' && i.dst === '$x' && (i.src as any).name === '$p0'
54
+ )).toBe(true)
55
+ })
56
+
57
+ it('fills in missing default arguments at call sites', () => {
58
+ const ir = compile(`
59
+ fn damage(amount: int, multiplier: int = 1) -> int {
60
+ return amount * multiplier;
61
+ }
62
+
63
+ fn test() -> int {
64
+ return damage(10);
65
+ }
66
+ `)
67
+ const fn = getFunction(ir, 'test')!
68
+ const call = getInstructions(fn).find(i => i.op === 'call') as any
69
+ expect(call.args).toHaveLength(2)
70
+ expect(call.args[0]).toEqual({ kind: 'const', value: 10 })
71
+ expect(call.args[1]).toEqual({ kind: 'const', value: 1 })
72
+ })
73
+
74
+ it('specializes callback-accepting functions for lambda arguments', () => {
75
+ const ir = compile(`
76
+ fn apply(val: int, cb: (int) -> int) -> int {
77
+ return cb(val);
78
+ }
79
+
80
+ fn test() -> int {
81
+ return apply(5, (x: int) => x * 3);
82
+ }
83
+ `)
84
+ expect(getFunction(ir, '__lambda_0')).toBeDefined()
85
+ const specialized = ir.functions.find(fn => fn.name.startsWith('apply__cb___lambda_0'))
86
+ expect(specialized).toBeDefined()
87
+ expect(specialized?.params).toEqual(['$val'])
88
+ const call = getInstructions(specialized!).find(i => i.op === 'call') as any
89
+ expect(call.fn).toBe('__lambda_0')
90
+ })
91
+ })
92
+
93
+ describe('let statements', () => {
94
+ it('inlines const values without allocating scoreboard variables', () => {
95
+ const ir = compile(`
96
+ const MAX_HP: int = 100
97
+
98
+ fn foo() {
99
+ let x: int = MAX_HP;
100
+ }
101
+ `)
102
+ const fn = getFunction(ir, 'foo')!
103
+ const instrs = getInstructions(fn)
104
+ expect(instrs.some(i =>
105
+ i.op === 'assign' && i.dst === '$x' && (i.src as any).kind === 'const' && (i.src as any).value === 100
106
+ )).toBe(true)
107
+ expect(ir.globals).not.toContain('$MAX_HP')
108
+ })
109
+
110
+ it('lowers let with literal', () => {
111
+ const ir = compile('fn foo() { let x: int = 42; }')
112
+ const fn = getFunction(ir, 'foo')!
113
+ const instrs = getInstructions(fn)
114
+ expect(instrs.some(i =>
115
+ i.op === 'assign' && i.dst === '$x' && (i.src as any).value === 42
116
+ )).toBe(true)
117
+ })
118
+
119
+ it('lowers let with expression', () => {
120
+ const ir = compile('fn foo(a: int) { let x: int = a + 1; }')
121
+ const fn = getFunction(ir, 'foo')!
122
+ const instrs = getInstructions(fn)
123
+ expect(instrs.some(i => i.op === 'binop')).toBe(true)
124
+ })
125
+
126
+ it('stores literal-backed string variables in storage for str_len', () => {
127
+ const ir = compile('fn foo() { let name: string = "Player"; let n: int = str_len(name); }')
128
+ const fn = getFunction(ir, 'foo')!
129
+ const rawCmds = getRawCommands(fn)
130
+ expect(rawCmds).toContain('data modify storage rs:strings name set value "Player"')
131
+ expect(rawCmds.some(cmd =>
132
+ cmd.includes('execute store result score') && cmd.includes('run data get storage rs:strings name')
133
+ )).toBe(true)
134
+ })
135
+ })
136
+
137
+ describe('return statements', () => {
138
+ it('lowers return with value', () => {
139
+ const ir = compile('fn foo() -> int { return 42; }')
140
+ const fn = getFunction(ir, 'foo')!
141
+ const term = fn.blocks[0].term
142
+ expect(term.op).toBe('return')
143
+ expect((term as any).value).toEqual({ kind: 'const', value: 42 })
144
+ })
145
+
146
+ it('lowers empty return', () => {
147
+ const ir = compile('fn foo() { return; }')
148
+ const fn = getFunction(ir, 'foo')!
149
+ const term = fn.blocks[0].term
150
+ expect(term.op).toBe('return')
151
+ expect((term as any).value).toBeUndefined()
152
+ })
153
+ })
154
+
155
+ describe('lambda lowering', () => {
156
+ it('lowers lambda variables to generated sub-functions', () => {
157
+ const ir = compile(`
158
+ fn test() {
159
+ let double: (int) -> int = (x: int) => x * 2;
160
+ let result: int = double(5);
161
+ }
162
+ `)
163
+ const lambdaFn = getFunction(ir, '__lambda_0')
164
+ expect(lambdaFn).toBeDefined()
165
+ const testFn = getFunction(ir, 'test')!
166
+ const calls = getInstructions(testFn).filter((instr): instr is IRInstr & { op: 'call' } => instr.op === 'call')
167
+ expect(calls.some(call => call.fn === '__lambda_0')).toBe(true)
168
+ })
169
+
170
+ it('inlines immediately-invoked expression-body lambdas', () => {
171
+ const ir = compile(`
172
+ fn test() -> int {
173
+ return ((x: int) => x * 2)(5);
174
+ }
175
+ `)
176
+ expect(ir.functions.find(fn => fn.name.startsWith('__lambda_'))).toBeUndefined()
177
+ const testFn = getFunction(ir, 'test')!
178
+ expect(getInstructions(testFn).some(instr => instr.op === 'binop')).toBe(true)
179
+ })
180
+ })
181
+
182
+ describe('binary expressions', () => {
183
+ it('lowers arithmetic', () => {
184
+ const ir = compile('fn foo(a: int, b: int) -> int { return a + b; }')
185
+ const fn = getFunction(ir, 'foo')!
186
+ const instrs = getInstructions(fn)
187
+ const binop = instrs.find(i => i.op === 'binop')
188
+ expect(binop).toBeDefined()
189
+ expect((binop as any).bop).toBe('+')
190
+ })
191
+
192
+ it('lowers comparison', () => {
193
+ const ir = compile('fn foo(a: int, b: int) -> bool { return a < b; }')
194
+ const fn = getFunction(ir, 'foo')!
195
+ const instrs = getInstructions(fn)
196
+ const cmp = instrs.find(i => i.op === 'cmp')
197
+ expect(cmp).toBeDefined()
198
+ expect((cmp as any).cop).toBe('<')
199
+ })
200
+ })
201
+
202
+ describe('unary expressions', () => {
203
+ it('lowers negation', () => {
204
+ const ir = compile('fn foo(x: int) -> int { return -x; }')
205
+ const fn = getFunction(ir, 'foo')!
206
+ const instrs = getInstructions(fn)
207
+ const binop = instrs.find(i => i.op === 'binop' && (i as any).bop === '-')
208
+ expect(binop).toBeDefined()
209
+ // -x is lowered as 0 - x
210
+ expect((binop as any).lhs).toEqual({ kind: 'const', value: 0 })
211
+ })
212
+
213
+ it('lowers logical not', () => {
214
+ const ir = compile('fn foo(x: bool) -> bool { return !x; }')
215
+ const fn = getFunction(ir, 'foo')!
216
+ const instrs = getInstructions(fn)
217
+ const cmp = instrs.find(i => i.op === 'cmp' && (i as any).cop === '==')
218
+ expect(cmp).toBeDefined()
219
+ // !x is lowered as x == 0
220
+ expect((cmp as any).rhs).toEqual({ kind: 'const', value: 0 })
221
+ })
222
+ })
223
+
224
+ describe('if statements', () => {
225
+ it('creates conditional jump', () => {
226
+ const ir = compile('fn foo(x: int) { if (x > 0) { let y: int = 1; } }')
227
+ const fn = getFunction(ir, 'foo')!
228
+ expect(fn.blocks.length).toBeGreaterThan(1)
229
+ const term = fn.blocks[0].term
230
+ expect(term.op).toBe('jump_if')
231
+ })
232
+
233
+ it('creates else block', () => {
234
+ const ir = compile('fn foo(x: int) { if (x > 0) { let y: int = 1; } else { let y: int = 2; } }')
235
+ const fn = getFunction(ir, 'foo')!
236
+ expect(fn.blocks.length).toBeGreaterThanOrEqual(3) // entry, then, else, merge
237
+ })
238
+ })
239
+
240
+ describe('while statements', () => {
241
+ it('creates loop structure', () => {
242
+ const ir = compile('fn foo() { let i: int = 0; while (i < 10) { i = i + 1; } }')
243
+ const fn = getFunction(ir, 'foo')!
244
+ // Should have: entry -> check -> body -> exit
245
+ expect(fn.blocks.length).toBeGreaterThanOrEqual(3)
246
+
247
+ // Find loop_check block
248
+ const checkBlock = fn.blocks.find(b => b.label.includes('loop_check'))
249
+ expect(checkBlock).toBeDefined()
250
+ })
251
+ })
252
+
253
+ describe('foreach statements', () => {
254
+ it('extracts body into sub-function', () => {
255
+ const ir = compile('fn kill_all() { foreach (e in @e[type=zombie]) { kill(e); } }')
256
+ expect(ir.functions.length).toBe(2) // main + foreach sub-function
257
+ const subFn = ir.functions.find(f => f.name.includes('foreach'))
258
+ expect(subFn).toBeDefined()
259
+ })
260
+
261
+ it('emits execute as ... run function', () => {
262
+ const ir = compile('fn kill_all() { foreach (e in @e[type=zombie]) { kill(e); } }')
263
+ const mainFn = getFunction(ir, 'kill_all')!
264
+ const rawCmds = getRawCommands(mainFn)
265
+ expect(rawCmds.some(cmd =>
266
+ cmd.includes('execute as @e[type=minecraft:zombie]') && cmd.includes('run function')
267
+ )).toBe(true)
268
+ })
269
+
270
+ it('binding maps to @s in sub-function', () => {
271
+ const ir = compile('fn kill_all() { foreach (e in @e[type=zombie]) { kill(e); } }')
272
+ const subFn = ir.functions.find(f => f.name.includes('foreach'))!
273
+ const rawCmds = getRawCommands(subFn)
274
+ expect(rawCmds.some(cmd => cmd === 'kill @s')).toBe(true)
275
+ })
276
+
277
+ it('lowers foreach over array into a counting loop', () => {
278
+ const ir = compile('fn walk() { let arr: int[] = [1, 2, 3]; foreach (x in arr) { let y: int = x; } }')
279
+ const fn = getFunction(ir, 'walk')!
280
+ expect(fn.blocks.some(b => b.label.includes('foreach_array_check'))).toBe(true)
281
+ expect(fn.blocks.some(b => b.label.includes('foreach_array_body'))).toBe(true)
282
+ const rawCmds = getRawCommands(fn)
283
+ expect(rawCmds.some(cmd => cmd.includes('data get storage rs:heap arr'))).toBe(true)
284
+ })
285
+ })
286
+
287
+ describe('match statements', () => {
288
+ it('lowers match into guarded execute function calls', () => {
289
+ const ir = compile('fn choose() { let choice: int = 2; match (choice) { 1 => { say("one"); } 2 => { say("two"); } _ => { say("other"); } } }')
290
+ const fn = getFunction(ir, 'choose')!
291
+ const rawCmds = getRawCommands(fn)
292
+ expect(rawCmds.some(cmd => cmd.includes('execute if score') && cmd.includes('matches 1 run function'))).toBe(true)
293
+ expect(rawCmds.some(cmd => cmd.includes('execute if score') && cmd.includes('matches 2 run function'))).toBe(true)
294
+ expect(rawCmds.some(cmd => cmd.includes('matches ..0 run function'))).toBe(true)
295
+ expect(ir.functions.filter(f => f.name.includes('match_')).length).toBe(3)
296
+ })
297
+
298
+ it('lowers enum variants to integer constants in comparisons and match arms', () => {
299
+ const ir = compile(`
300
+ enum Direction { North, South, East, West }
301
+
302
+ fn choose(dir: Direction) {
303
+ if (dir == Direction.South) {
304
+ say("south");
305
+ }
306
+ match (dir) {
307
+ Direction.North => { say("north"); }
308
+ Direction.South => { say("south"); }
309
+ _ => { say("other"); }
310
+ }
311
+ }
312
+ `)
313
+ const fn = getFunction(ir, 'choose')!
314
+ const rawCmds = getRawCommands(fn)
315
+ expect(getInstructions(fn).some(i => i.op === 'cmp' && (i as any).rhs.value === 1)).toBe(true)
316
+ expect(rawCmds.some(cmd => cmd.includes('matches 0 run function'))).toBe(true)
317
+ expect(rawCmds.some(cmd => cmd.includes('matches 1 run function'))).toBe(true)
318
+ })
319
+ })
320
+
321
+ describe('arrays', () => {
322
+ it('lowers array literal initialization', () => {
323
+ const ir = compile('fn test() { let arr: int[] = [1, 2, 3]; }')
324
+ const fn = getFunction(ir, 'test')!
325
+ const rawCmds = getRawCommands(fn)
326
+ expect(rawCmds).toContain('data modify storage rs:heap arr set value []')
327
+ expect(rawCmds).toContain('data modify storage rs:heap arr append value 1')
328
+ expect(rawCmds).toContain('data modify storage rs:heap arr append value 2')
329
+ expect(rawCmds).toContain('data modify storage rs:heap arr append value 3')
330
+ })
331
+
332
+ it('lowers array len property', () => {
333
+ const ir = compile('fn test() { let arr: int[] = [1]; let n: int = arr.len; }')
334
+ const fn = getFunction(ir, 'test')!
335
+ const rawCmds = getRawCommands(fn)
336
+ expect(rawCmds.some(cmd =>
337
+ cmd.includes('execute store result score') && cmd.includes('run data get storage rs:heap arr')
338
+ )).toBe(true)
339
+ })
340
+
341
+ it('lowers static array indexing', () => {
342
+ const ir = compile('fn test() { let arr: int[] = [7, 8]; let x: int = arr[0]; }')
343
+ const fn = getFunction(ir, 'test')!
344
+ const rawCmds = getRawCommands(fn)
345
+ expect(rawCmds.some(cmd => cmd.includes('run data get storage rs:heap arr[0]'))).toBe(true)
346
+ })
347
+
348
+ it('lowers dynamic array indexing via macro helper', () => {
349
+ const ir = compile('fn test() { let arr: int[] = [7, 8]; let i: int = 1; let x: int = arr[i]; }')
350
+ const fn = getFunction(ir, 'test')!
351
+ const rawCmds = getRawCommands(fn)
352
+ expect(rawCmds.some(cmd => cmd.includes('with storage rs:heap'))).toBe(true)
353
+ const helperFn = ir.functions.find(f => f.name.includes('array_get_'))
354
+ expect(helperFn).toBeDefined()
355
+ expect(getRawCommands(helperFn!).some(cmd => cmd.includes('arr[$('))).toBe(true)
356
+ })
357
+
358
+ it('lowers array push', () => {
359
+ const ir = compile('fn test() { let arr: int[] = []; arr.push(4); }')
360
+ const fn = getFunction(ir, 'test')!
361
+ const rawCmds = getRawCommands(fn)
362
+ expect(rawCmds).toContain('data modify storage rs:heap arr append value 4')
363
+ })
364
+
365
+ it('lowers array pop', () => {
366
+ const ir = compile('fn test() { let arr: int[] = [1, 2]; let x: int = arr.pop(); }')
367
+ const fn = getFunction(ir, 'test')!
368
+ const rawCmds = getRawCommands(fn)
369
+ expect(rawCmds.some(cmd => cmd.includes('run data get storage rs:heap arr[-1]'))).toBe(true)
370
+ expect(rawCmds).toContain('data remove storage rs:heap arr[-1]')
371
+ })
372
+ })
373
+
374
+ describe('as/at blocks', () => {
375
+ it('extracts as block into sub-function', () => {
376
+ const ir = compile('fn test() { as @a { say("hello"); } }')
377
+ expect(ir.functions.length).toBe(2)
378
+ const mainFn = getFunction(ir, 'test')!
379
+ const rawCmds = getRawCommands(mainFn)
380
+ expect(rawCmds.some(cmd =>
381
+ cmd.includes('execute as @a') && cmd.includes('run function')
382
+ )).toBe(true)
383
+ })
384
+
385
+ it('extracts at block into sub-function', () => {
386
+ const ir = compile('fn test() { at @s { summon("zombie"); } }')
387
+ expect(ir.functions.length).toBe(2)
388
+ const mainFn = getFunction(ir, 'test')!
389
+ const rawCmds = getRawCommands(mainFn)
390
+ expect(rawCmds.some(cmd =>
391
+ cmd.includes('execute at @s') && cmd.includes('run function')
392
+ )).toBe(true)
393
+ })
394
+ })
395
+
396
+ describe('execute inline blocks', () => {
397
+ it('extracts execute as run block into sub-function', () => {
398
+ const ir = compile('fn test() { execute as @a run { say("hello from each"); } }')
399
+ expect(ir.functions.length).toBe(2)
400
+ const mainFn = getFunction(ir, 'test')!
401
+ const rawCmds = getRawCommands(mainFn)
402
+ expect(rawCmds.some(cmd =>
403
+ cmd.includes('execute as @a run function')
404
+ )).toBe(true)
405
+ })
406
+
407
+ it('extracts execute as at run block into sub-function', () => {
408
+ const ir = compile('fn test() { execute as @a at @s run { particle("flame"); } }')
409
+ expect(ir.functions.length).toBe(2)
410
+ const mainFn = getFunction(ir, 'test')!
411
+ const rawCmds = getRawCommands(mainFn)
412
+ expect(rawCmds.some(cmd =>
413
+ cmd.includes('execute as @a at @s run function')
414
+ )).toBe(true)
415
+ })
416
+
417
+ it('handles execute with if entity condition', () => {
418
+ const ir = compile('fn test() { execute as @a if entity @s[tag=admin] run { give(@s, "diamond", 1); } }')
419
+ expect(ir.functions.length).toBe(2)
420
+ const mainFn = getFunction(ir, 'test')!
421
+ const rawCmds = getRawCommands(mainFn)
422
+ expect(rawCmds.some(cmd =>
423
+ cmd.includes('execute as @a if entity @s[tag=admin] run function')
424
+ )).toBe(true)
425
+ })
426
+
427
+ it('handles execute with unless entity condition', () => {
428
+ const ir = compile('fn test() { execute as @a unless entity @s[tag=dead] run { effect(@s, "regeneration", 5); } }')
429
+ expect(ir.functions.length).toBe(2)
430
+ const mainFn = getFunction(ir, 'test')!
431
+ const rawCmds = getRawCommands(mainFn)
432
+ expect(rawCmds.some(cmd =>
433
+ cmd.includes('execute as @a unless entity @s[tag=dead] run function')
434
+ )).toBe(true)
435
+ })
436
+
437
+ it('handles execute with in dimension', () => {
438
+ const ir = compile('fn test() { execute in the_nether run { say("in nether"); } }')
439
+ expect(ir.functions.length).toBe(2)
440
+ const mainFn = getFunction(ir, 'test')!
441
+ const rawCmds = getRawCommands(mainFn)
442
+ expect(rawCmds.some(cmd =>
443
+ cmd.includes('execute in the_nether run function')
444
+ )).toBe(true)
445
+ })
446
+
447
+ it('lowered sub-function contains body commands', () => {
448
+ const ir = compile('fn test() { execute as @a run { say("inner"); give(@s, "bread", 1); } }')
449
+ const subFn = ir.functions.find(f => f.name.includes('exec_'))!
450
+ expect(subFn).toBeDefined()
451
+ const rawCmds = getRawCommands(subFn)
452
+ expect(rawCmds).toContain('say inner')
453
+ expect(rawCmds.some(cmd => cmd.includes('give @s bread 1'))).toBe(true)
454
+ })
455
+ })
456
+
457
+ describe('builtins', () => {
458
+ it('lowers say()', () => {
459
+ const ir = compile('fn test() { say("hello"); }')
460
+ const fn = getFunction(ir, 'test')!
461
+ const rawCmds = getRawCommands(fn)
462
+ expect(rawCmds).toContain('say hello')
463
+ })
464
+
465
+ it('lowers kill()', () => {
466
+ const ir = compile('fn test() { kill(@e[type=zombie]); }')
467
+ const fn = getFunction(ir, 'test')!
468
+ const rawCmds = getRawCommands(fn)
469
+ expect(rawCmds).toContain('kill @e[type=minecraft:zombie]')
470
+ })
471
+
472
+ it('lowers give()', () => {
473
+ const ir = compile('fn test() { give(@p, "diamond", 64); }')
474
+ const fn = getFunction(ir, 'test')!
475
+ const rawCmds = getRawCommands(fn)
476
+ expect(rawCmds).toContain('give @p diamond 64')
477
+ })
478
+
479
+ it('lowers actionbar(), subtitle(), and title_times()', () => {
480
+ const ir = compile('fn test() { actionbar(@a, "Fight!"); subtitle(@a, "Next wave"); title_times(@a, 10, 40, 10); }')
481
+ const fn = getFunction(ir, 'test')!
482
+ const rawCmds = getRawCommands(fn)
483
+ expect(rawCmds).toContain('title @a actionbar {"text":"Fight!"}')
484
+ expect(rawCmds).toContain('title @a subtitle {"text":"Next wave"}')
485
+ expect(rawCmds).toContain('title @a times 10 40 10')
486
+ })
487
+
488
+ it('lowers announce()', () => {
489
+ const ir = compile('fn test() { announce("Server event starting"); }')
490
+ const fn = getFunction(ir, 'test')!
491
+ const rawCmds = getRawCommands(fn)
492
+ expect(rawCmds).toContain('tellraw @a {"text":"Server event starting"}')
493
+ })
494
+
495
+ it('lowers interpolated say() to tellraw score components', () => {
496
+ const ir = compile('fn test() { let score: int = 7; say("You have ${score} points"); }')
497
+ const fn = getFunction(ir, 'test')!
498
+ const rawCmds = getRawCommands(fn)
499
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$score","objective":"rs"}},{"text":" points"}]')
500
+ })
501
+
502
+ it('lowers summon()', () => {
503
+ const ir = compile('fn test() { summon("zombie"); }')
504
+ const fn = getFunction(ir, 'test')!
505
+ const rawCmds = getRawCommands(fn)
506
+ expect(rawCmds.some(cmd => cmd.includes('summon zombie'))).toBe(true)
507
+ })
508
+
509
+ it('lowers effect()', () => {
510
+ const ir = compile('fn test() { effect(@a, "speed", 30, 1); }')
511
+ const fn = getFunction(ir, 'test')!
512
+ const rawCmds = getRawCommands(fn)
513
+ expect(rawCmds.some(cmd => cmd.includes('effect give @a speed 30 1'))).toBe(true)
514
+ })
515
+
516
+ it('lowers tp() for both positions and entity destinations', () => {
517
+ const ir = compile('fn test() { tp(@s, (~1, ~0, ~-1)); tp(@s, "^0", "^1", "^0"); tp(@s, @p); tp(@a, (1, 64, 1)); }')
518
+ const fn = getFunction(ir, 'test')!
519
+ const rawCmds = getRawCommands(fn)
520
+ expect(rawCmds).toContain('tp @s ~1 ~ ~-1')
521
+ expect(rawCmds).toContain('tp @s ^0 ^1 ^0')
522
+ expect(rawCmds).toContain('tp @s @p')
523
+ expect(rawCmds).toContain('tp @a 1 64 1')
524
+ })
525
+
526
+ it('warns when using tp_to()', () => {
527
+ const { ir, warnings } = compileWithWarnings('fn test() { tp_to(@s, @p); }')
528
+ const fn = getFunction(ir, 'test')!
529
+ const rawCmds = getRawCommands(fn)
530
+ expect(rawCmds).toContain('tp @s @p')
531
+ expect(warnings).toContainEqual(expect.objectContaining({
532
+ code: 'W_DEPRECATED',
533
+ message: 'tp_to is deprecated; use tp instead',
534
+ }))
535
+ })
536
+
537
+ it('lowers inventory and player admin commands', () => {
538
+ const ir = compile('fn test() { clear(@s); clear(@s, "minecraft:stick"); kick(@p); kick(@p, "AFK"); }')
539
+ const fn = getFunction(ir, 'test')!
540
+ const rawCmds = getRawCommands(fn)
541
+ expect(rawCmds).toContain('clear @s')
542
+ expect(rawCmds).toContain('clear @s minecraft:stick')
543
+ expect(rawCmds).toContain('kick @p')
544
+ expect(rawCmds).toContain('kick @p AFK')
545
+ })
546
+
547
+ it('lowers world management commands', () => {
548
+ const ir = compile('fn test() { weather("rain"); time_set("day"); time_add(1000); gamerule("doDaylightCycle", "false"); difficulty("hard"); }')
549
+ const fn = getFunction(ir, 'test')!
550
+ const rawCmds = getRawCommands(fn)
551
+ expect(rawCmds).toContain('weather rain')
552
+ expect(rawCmds).toContain('time set day')
553
+ expect(rawCmds).toContain('time add 1000')
554
+ expect(rawCmds).toContain('gamerule doDaylightCycle false')
555
+ expect(rawCmds).toContain('difficulty hard')
556
+ })
557
+
558
+ it('lowers tag_add() and tag_remove()', () => {
559
+ const ir = compile('fn test() { tag_add(@s, "boss"); tag_remove(@s, "boss"); }')
560
+ const fn = getFunction(ir, 'test')!
561
+ const rawCmds = getRawCommands(fn)
562
+ expect(rawCmds).toContain('tag @s add boss')
563
+ expect(rawCmds).toContain('tag @s remove boss')
564
+ })
565
+
566
+ it('lowers setblock(), fill(), and clone()', () => {
567
+ const ir = compile('fn test() { setblock((4, 65, 4), "stone"); fill((0, 64, 0), (8, 64, 8), "minecraft:smooth_stone"); clone((0, 64, 0), (4, 68, 4), (10, 64, 10)); setblock("~", "~", "~", "legacy"); }')
568
+ const fn = getFunction(ir, 'test')!
569
+ const rawCmds = getRawCommands(fn)
570
+ expect(rawCmds).toContain('setblock 4 65 4 stone')
571
+ expect(rawCmds).toContain('fill 0 64 0 8 64 8 minecraft:smooth_stone')
572
+ expect(rawCmds).toContain('clone 0 64 0 4 68 4 10 64 10')
573
+ expect(rawCmds).toContain('setblock ~ ~ ~ legacy')
574
+ })
575
+
576
+ it('lowers BlockPos locals in coordinate builtins', () => {
577
+ const ir = compile('fn test() { let spawn: BlockPos = (4, 65, 4); setblock(spawn, "minecraft:stone"); }')
578
+ const fn = getFunction(ir, 'test')!
579
+ const rawCmds = getRawCommands(fn)
580
+ expect(rawCmds).toContain('setblock 4 65 4 minecraft:stone')
581
+ })
582
+
583
+ it('lowers xp_add() and xp_set()', () => {
584
+ const ir = compile('fn test() { xp_add(@s, 5); xp_add(@s, 2, "levels"); xp_set(@s, 0); xp_set(@s, 3, "levels"); }')
585
+ const fn = getFunction(ir, 'test')!
586
+ const rawCmds = getRawCommands(fn)
587
+ expect(rawCmds).toContain('xp add @s 5 points')
588
+ expect(rawCmds).toContain('xp add @s 2 levels')
589
+ expect(rawCmds).toContain('xp set @s 0 points')
590
+ expect(rawCmds).toContain('xp set @s 3 levels')
591
+ })
592
+
593
+ it('lowers scoreboard display and objective management builtins', () => {
594
+ const ir = compile(`
595
+ fn test() {
596
+ scoreboard_display("sidebar", "kills");
597
+ scoreboard_display("list", "coins");
598
+ scoreboard_display("belowName", "hp");
599
+ scoreboard_hide("sidebar");
600
+ scoreboard_add_objective("kills", "playerKillCount", "Kill Count");
601
+ scoreboard_remove_objective("kills");
602
+ }
603
+ `)
604
+ const fn = getFunction(ir, 'test')!
605
+ const rawCmds = getRawCommands(fn)
606
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar kills')
607
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list coins')
608
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName hp')
609
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar')
610
+ expect(rawCmds).toContain('scoreboard objectives add kills playerKillCount "Kill Count"')
611
+ expect(rawCmds).toContain('scoreboard objectives remove kills')
612
+ })
613
+
614
+ it('lowers bossbar management builtins', () => {
615
+ const ir = compile(`
616
+ fn test() {
617
+ bossbar_add("ns:health", "Boss Health");
618
+ bossbar_set_value("ns:health", 50);
619
+ bossbar_set_max("ns:health", 100);
620
+ bossbar_set_color("ns:health", "red");
621
+ bossbar_set_style("ns:health", "notched_10");
622
+ bossbar_set_visible("ns:health", true);
623
+ bossbar_set_players("ns:health", @a);
624
+ bossbar_remove("ns:health");
625
+ let current: int = bossbar_get_value("ns:health");
626
+ }
627
+ `)
628
+ const fn = getFunction(ir, 'test')!
629
+ const rawCmds = getRawCommands(fn)
630
+ expect(rawCmds).toContain('bossbar add ns:health {"text":"Boss Health"}')
631
+ expect(rawCmds).toContain('bossbar set ns:health value 50')
632
+ expect(rawCmds).toContain('bossbar set ns:health max 100')
633
+ expect(rawCmds).toContain('bossbar set ns:health color red')
634
+ expect(rawCmds).toContain('bossbar set ns:health style notched_10')
635
+ expect(rawCmds).toContain('bossbar set ns:health visible true')
636
+ expect(rawCmds).toContain('bossbar set ns:health players @a')
637
+ expect(rawCmds).toContain('bossbar remove ns:health')
638
+ expect(rawCmds.some(cmd => /^execute store result score \$t\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true)
639
+ })
640
+
641
+ it('lowers team management builtins', () => {
642
+ const ir = compile(`
643
+ fn test() {
644
+ team_add("red", "Red Team");
645
+ team_remove("red");
646
+ team_join("red", @a[tag=red_team]);
647
+ team_leave(@s);
648
+ team_option("red", "friendlyFire", "false");
649
+ team_option("red", "color", "red");
650
+ team_option("red", "prefix", "[Red] ");
651
+ }
652
+ `)
653
+ const fn = getFunction(ir, 'test')!
654
+ const rawCmds = getRawCommands(fn)
655
+ expect(rawCmds).toContain('team add red {"text":"Red Team"}')
656
+ expect(rawCmds).toContain('team remove red')
657
+ expect(rawCmds).toContain('team join red @a[tag=red_team]')
658
+ expect(rawCmds).toContain('team leave @s')
659
+ expect(rawCmds).toContain('team modify red friendlyFire false')
660
+ expect(rawCmds).toContain('team modify red color red')
661
+ expect(rawCmds).toContain('team modify red prefix {"text":"[Red] "}')
662
+ })
663
+
664
+ it('lowers random()', () => {
665
+ const ir = compile('fn test() { let x: int = random(1, 100); }')
666
+ const fn = getFunction(ir, 'test')!
667
+ const rawCmds = getRawCommands(fn)
668
+ expect(rawCmds).toContain('scoreboard players random $t0 rs 1 100')
669
+ })
670
+
671
+ it('lowers random_native()', () => {
672
+ const ir = compile('fn test() { let x: int = random_native(1, 6); }')
673
+ const fn = getFunction(ir, 'test')!
674
+ const rawCmds = getRawCommands(fn)
675
+ expect(rawCmds).toContain('execute store result score $t0 rs run random value 1 6')
676
+ })
677
+
678
+ it('lowers random_sequence()', () => {
679
+ const ir = compile('fn test() { random_sequence("loot", 42); }')
680
+ const fn = getFunction(ir, 'test')!
681
+ const rawCmds = getRawCommands(fn)
682
+ expect(rawCmds).toContain('random reset loot 42')
683
+ })
684
+
685
+ it('lowers data_get from entity', () => {
686
+ const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }')
687
+ const fn = getFunction(ir, 'test')!
688
+ const rawCmds = getRawCommands(fn)
689
+ expect(rawCmds.some(cmd =>
690
+ cmd.includes('execute store result score') &&
691
+ cmd.includes('run data get entity @s SelectedItem.Count 1')
692
+ )).toBe(true)
693
+ })
694
+
695
+ it('lowers data_get from block', () => {
696
+ const ir = compile('fn test() { let furnace_fuel: int = data_get("block", "~ ~ ~", "BurnTime"); }')
697
+ const fn = getFunction(ir, 'test')!
698
+ const rawCmds = getRawCommands(fn)
699
+ expect(rawCmds.some(cmd =>
700
+ cmd.includes('run data get block ~ ~ ~ BurnTime 1')
701
+ )).toBe(true)
702
+ })
703
+
704
+ it('lowers data_get from storage', () => {
705
+ const ir = compile('fn test() { let val: int = data_get("storage", "mypack:globals", "player_count"); }')
706
+ const fn = getFunction(ir, 'test')!
707
+ const rawCmds = getRawCommands(fn)
708
+ expect(rawCmds.some(cmd =>
709
+ cmd.includes('run data get storage mypack:globals player_count 1')
710
+ )).toBe(true)
711
+ })
712
+
713
+ it('lowers data_get with scale factor', () => {
714
+ const ir = compile('fn test() { let scaled: int = data_get("entity", "@s", "Pos[0]", "1000"); }')
715
+ const fn = getFunction(ir, 'test')!
716
+ const rawCmds = getRawCommands(fn)
717
+ expect(rawCmds.some(cmd =>
718
+ cmd.includes('run data get entity @s Pos[0] 1000')
719
+ )).toBe(true)
720
+ })
721
+
722
+ it('data_get result can be used in expressions', () => {
723
+ const ir = compile(`
724
+ fn test() {
725
+ let count: int = data_get("entity", "@s", "SelectedItem.Count");
726
+ let doubled: int = count * 2;
727
+ }
728
+ `)
729
+ const fn = getFunction(ir, 'test')!
730
+ const instrs = getInstructions(fn)
731
+ expect(instrs.some(i => i.op === 'binop' && (i as any).bop === '*')).toBe(true)
732
+ })
733
+
734
+ it('accepts bare selector targets in scoreboard_get', () => {
735
+ const ir = compile('fn test() { let score: int = scoreboard_get(@s, "score"); }')
736
+ const fn = getFunction(ir, 'test')!
737
+ const rawCmds = getRawCommands(fn)
738
+ expect(rawCmds.some(cmd =>
739
+ cmd.includes('run scoreboard players get @s score')
740
+ )).toBe(true)
741
+ })
742
+
743
+ it('accepts bare selector targets in scoreboard_set', () => {
744
+ const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }')
745
+ const fn = getFunction(ir, 'test')!
746
+ const rawCmds = getRawCommands(fn)
747
+ expect(rawCmds).toContain('scoreboard players set @a kills 0')
748
+ })
749
+
750
+ it('warns on quoted selectors in scoreboard_get', () => {
751
+ const { ir, warnings } = compileWithWarnings('fn test() { let score: int = scoreboard_get("@s", "score"); }')
752
+ const fn = getFunction(ir, 'test')!
753
+ const rawCmds = getRawCommands(fn)
754
+ expect(rawCmds.some(cmd =>
755
+ cmd.includes('run scoreboard players get @s score')
756
+ )).toBe(true)
757
+ expect(warnings).toContainEqual(expect.objectContaining({
758
+ code: 'W_QUOTED_SELECTOR',
759
+ message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
760
+ }))
761
+ })
762
+
763
+ it('does not warn on fake player names', () => {
764
+ const { ir, warnings } = compileWithWarnings('fn test() { let total: int = scoreboard_get("#global", "total"); }')
765
+ const fn = getFunction(ir, 'test')!
766
+ const rawCmds = getRawCommands(fn)
767
+ expect(rawCmds.some(cmd =>
768
+ cmd.includes('run scoreboard players get #global total')
769
+ )).toBe(true)
770
+ expect(warnings).toHaveLength(0)
771
+ })
772
+
773
+ it('warns on quoted selectors in data_get entity targets', () => {
774
+ const { ir, warnings } = compileWithWarnings('fn test() { let pos: int = data_get("entity", "@s", "Pos[0]"); }')
775
+ const fn = getFunction(ir, 'test')!
776
+ const rawCmds = getRawCommands(fn)
777
+ expect(rawCmds.some(cmd =>
778
+ cmd.includes('run data get entity @s Pos[0] 1')
779
+ )).toBe(true)
780
+ expect(warnings).toContainEqual(expect.objectContaining({
781
+ code: 'W_QUOTED_SELECTOR',
782
+ message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
783
+ }))
784
+ })
785
+ })
786
+
787
+ describe('decorators', () => {
788
+ it('marks @tick function', () => {
789
+ const ir = compile('@tick fn game_loop() {}')
790
+ const fn = getFunction(ir, 'game_loop')!
791
+ expect(fn.isTickLoop).toBe(true)
792
+ })
793
+
794
+ it('marks @on_trigger function', () => {
795
+ const ir = compile('@on_trigger("my_trigger") fn handle_trigger() {}')
796
+ const fn = getFunction(ir, 'handle_trigger')!
797
+ expect(fn.isTriggerHandler).toBe(true)
798
+ expect(fn.triggerName).toBe('my_trigger')
799
+ })
800
+
801
+ it('marks @on_advancement function for advancement json generation', () => {
802
+ const ir = compile('@on_advancement("story/mine_diamond") fn handle_advancement() {}')
803
+ const fn = getFunction(ir, 'handle_advancement')!
804
+ expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' })
805
+ })
806
+ })
807
+
808
+ describe('selectors', () => {
809
+ it('converts selector with filters to string', () => {
810
+ const ir = compile('fn test() { kill(@e[type=zombie, distance=..10, tag=boss]); }')
811
+ const fn = getFunction(ir, 'test')!
812
+ const rawCmds = getRawCommands(fn)
813
+ const killCmd = rawCmds.find(cmd => cmd.startsWith('kill'))
814
+ expect(killCmd).toContain('type=minecraft:zombie')
815
+ expect(killCmd).toContain('distance=..10')
816
+ expect(killCmd).toContain('tag=boss')
817
+ })
818
+
819
+ it('warns and auto-qualifies unnamespaced entity types', () => {
820
+ const { ir, warnings } = compileWithWarnings('fn test() { kill(@e[type=zombie]); }')
821
+ const fn = getFunction(ir, 'test')!
822
+ const rawCmds = getRawCommands(fn)
823
+ expect(rawCmds).toContain('kill @e[type=minecraft:zombie]')
824
+ expect(warnings).toContainEqual({
825
+ code: 'W_UNNAMESPACED_TYPE',
826
+ message: 'Unnamespaced entity type "zombie", auto-qualifying to "minecraft:zombie"',
827
+ })
828
+ })
829
+
830
+ it('passes through minecraft entity types without warnings', () => {
831
+ const { ir, warnings } = compileWithWarnings('fn test() { kill(@e[type=minecraft:zombie]); }')
832
+ const fn = getFunction(ir, 'test')!
833
+ const rawCmds = getRawCommands(fn)
834
+ expect(rawCmds).toContain('kill @e[type=minecraft:zombie]')
835
+ expect(warnings).toHaveLength(0)
836
+ })
837
+
838
+ it('passes through custom namespaced entity types without warnings', () => {
839
+ const { ir, warnings } = compileWithWarnings('fn test() { kill(@e[type=my_mod:custom_mob]); }')
840
+ const fn = getFunction(ir, 'test')!
841
+ const rawCmds = getRawCommands(fn)
842
+ expect(rawCmds).toContain('kill @e[type=my_mod:custom_mob]')
843
+ expect(warnings).toHaveLength(0)
844
+ })
845
+
846
+ it('throws on invalid entity type format', () => {
847
+ expect(() => compileWithWarnings('fn test() { kill(@e[type=invalid!!!]); }'))
848
+ .toThrow('Invalid entity type format: "invalid!!!"')
849
+ })
850
+ })
851
+
852
+ describe('raw commands', () => {
853
+ it('passes through raw commands', () => {
854
+ const ir = compile('fn test() { raw("tp @a ~ ~10 ~"); }')
855
+ const fn = getFunction(ir, 'test')!
856
+ const rawCmds = getRawCommands(fn)
857
+ expect(rawCmds).toContain('tp @a ~ ~10 ~')
858
+ })
859
+ })
860
+
861
+ describe('assignment operators', () => {
862
+ it('lowers compound assignment', () => {
863
+ const ir = compile('fn test() { let x: int = 5; x += 3; }')
864
+ const fn = getFunction(ir, 'test')!
865
+ const instrs = getInstructions(fn)
866
+ const binop = instrs.find(i => i.op === 'binop' && (i as any).bop === '+')
867
+ expect(binop).toBeDefined()
868
+ })
869
+ })
870
+
871
+ describe('entity tag methods', () => {
872
+ it('lowers entity.tag()', () => {
873
+ const ir = compile('fn test() { @s.tag("boss"); }')
874
+ const fn = getFunction(ir, 'test')!
875
+ const rawCmds = getRawCommands(fn)
876
+ expect(rawCmds).toContain('tag @s add boss')
877
+ })
878
+
879
+ it('lowers entity.untag()', () => {
880
+ const ir = compile('fn test() { @s.untag("boss"); }')
881
+ const fn = getFunction(ir, 'test')!
882
+ const rawCmds = getRawCommands(fn)
883
+ expect(rawCmds).toContain('tag @s remove boss')
884
+ })
885
+
886
+ it('lowers entity.has_tag() and returns temp var', () => {
887
+ const ir = compile('fn test() { let x: bool = @s.has_tag("boss"); }')
888
+ const fn = getFunction(ir, 'test')!
889
+ const rawCmds = getRawCommands(fn)
890
+ expect(rawCmds.some(cmd =>
891
+ cmd.includes('execute store result score') && cmd.includes('if entity @s[tag=boss]')
892
+ )).toBe(true)
893
+ })
894
+
895
+ it('lowers entity.tag() on selector with filters', () => {
896
+ const ir = compile('fn test() { @e[type=zombie].tag("marked"); }')
897
+ const fn = getFunction(ir, 'test')!
898
+ const rawCmds = getRawCommands(fn)
899
+ expect(rawCmds.some(cmd =>
900
+ cmd.includes('tag @e[type=minecraft:zombie] add marked')
901
+ )).toBe(true)
902
+ })
903
+ })
904
+
905
+ describe('complex programs', () => {
906
+ it('compiles add function correctly', () => {
907
+ const source = `
908
+ fn add(a: int, b: int) -> int {
909
+ return a + b;
910
+ }
911
+ `
912
+ const ir = compile(source)
913
+ const fn = getFunction(ir, 'add')!
914
+ expect(fn.params).toEqual(['$a', '$b'])
915
+
916
+ const instrs = getInstructions(fn)
917
+ expect(instrs.some(i => i.op === 'binop' && (i as any).bop === '+')).toBe(true)
918
+
919
+ const term = fn.blocks[fn.blocks.length - 1].term
920
+ expect(term.op).toBe('return')
921
+ expect((term as any).value?.kind).toBe('var')
922
+ })
923
+
924
+ it('compiles abs function with if/else', () => {
925
+ const source = `
926
+ fn abs(x: int) -> int {
927
+ if (x < 0) {
928
+ return -x;
929
+ } else {
930
+ return x;
931
+ }
932
+ }
933
+ `
934
+ const ir = compile(source)
935
+ const fn = getFunction(ir, 'abs')!
936
+ expect(fn.blocks.length).toBeGreaterThanOrEqual(3)
937
+
938
+ // Should have comparison
939
+ const instrs = getInstructions(fn)
940
+ expect(instrs.some(i => i.op === 'cmp' && (i as any).cop === '<')).toBe(true)
941
+ })
942
+
943
+ it('compiles countdown with while', () => {
944
+ const source = `
945
+ fn count_down() {
946
+ let i: int = 10;
947
+ while (i > 0) {
948
+ i = i - 1;
949
+ }
950
+ }
951
+ `
952
+ const ir = compile(source)
953
+ const fn = getFunction(ir, 'count_down')!
954
+
955
+ // Should have loop structure
956
+ const checkBlock = fn.blocks.find(b => b.label.includes('loop_check'))
957
+ const bodyBlock = fn.blocks.find(b => b.label.includes('loop_body'))
958
+ expect(checkBlock).toBeDefined()
959
+ expect(bodyBlock).toBeDefined()
960
+ })
961
+ })
962
+ })