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
@@ -0,0 +1,1789 @@
1
+ "use strict";
2
+ /**
3
+ * End-to-End Tests
4
+ *
5
+ * Tests the complete pipeline: Source → Lexer → Parser → HIR → MIR → LIR → emit
6
+ */
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ const lexer_1 = require("../lexer");
9
+ const parser_1 = require("../parser");
10
+ const typechecker_1 = require("../typechecker");
11
+ const compile_1 = require("../../src2/emit/compile");
12
+ // ---------------------------------------------------------------------------
13
+ // Test Helpers
14
+ // ---------------------------------------------------------------------------
15
+ function compile(source, namespace = 'test') {
16
+ const result = (0, compile_1.compile)(source, { namespace });
17
+ return result.files;
18
+ }
19
+ function getFunction(files, name) {
20
+ const file = files.find(f => f.path.includes(`/${name}.mcfunction`));
21
+ return file?.content;
22
+ }
23
+ function getSubFunction(files, parent, sub) {
24
+ const file = files.find(f => f.path.includes(`/${parent}/${sub}.mcfunction`));
25
+ return file?.content;
26
+ }
27
+ function hasTickTag(files, namespace, fnName) {
28
+ // Check if the function is called from __tick.mcfunction
29
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
30
+ if (!tickFn)
31
+ return false;
32
+ return tickFn.content.includes(`function ${namespace}:${fnName}`);
33
+ }
34
+ function typeCheck(source) {
35
+ const tokens = new lexer_1.Lexer(source).tokenize();
36
+ const ast = new parser_1.Parser(tokens, source).parse('test');
37
+ return new typechecker_1.TypeChecker(source).check(ast);
38
+ }
39
+ // ---------------------------------------------------------------------------
40
+ // Tests
41
+ // ---------------------------------------------------------------------------
42
+ describe('E2E: Complete Pipeline', () => {
43
+ describe('const declarations', () => {
44
+ it('inlines consts in expressions and string interpolation', () => {
45
+ const files = compile(`
46
+ const MAX_HP: int = 100
47
+ const GAME_NAME: string = "Arena Battle"
48
+
49
+ fn main() {
50
+ let hp: int = MAX_HP + 5;
51
+ announce("\${GAME_NAME}: \${hp}");
52
+ }
53
+ `);
54
+ const mainFn = getFunction(files, 'main');
55
+ expect(mainFn).toBeDefined();
56
+ expect(mainFn).toContain('scoreboard players set $main_hp rs 105');
57
+ expect(mainFn).toContain('Arena Battle');
58
+ });
59
+ });
60
+ describe('string stdlib helpers', () => {
61
+ it('lowers str_len for literal-backed string variables', () => {
62
+ const files = compile(`
63
+ fn main() {
64
+ let name: string = "Player";
65
+ let n: int = str_len(name);
66
+ tell(@s, "\${n}");
67
+ }
68
+ `);
69
+ const mainFn = getFunction(files, 'main');
70
+ expect(mainFn).toBeDefined();
71
+ expect(mainFn).toContain('data modify storage rs:strings name set value "Player"');
72
+ expect(mainFn).toContain('run data get storage rs:strings name');
73
+ expect(mainFn).toContain('"objective":"rs"');
74
+ });
75
+ });
76
+ describe('timer builtins', () => {
77
+ it('generates scheduled timer helper functions', () => {
78
+ const files = compile(`
79
+ fn main() {
80
+ let intervalId: int = setInterval(20, () => {
81
+ say("tick");
82
+ });
83
+ setTimeout(100, () => {
84
+ say("later");
85
+ });
86
+ clearInterval(intervalId);
87
+ }
88
+ `);
89
+ const mainFn = getFunction(files, 'main');
90
+ const intervalFn = getFunction(files, '__interval_0');
91
+ const intervalBodyFn = getFunction(files, '__interval_body_0');
92
+ const timeoutFn = getFunction(files, '__timeout_0');
93
+ expect(mainFn).toBeDefined();
94
+ expect(mainFn).toContain('schedule function test:__interval_0 20t');
95
+ expect(mainFn).toContain('schedule function test:__timeout_0 100t');
96
+ expect(mainFn).toContain('schedule clear test:__interval_0');
97
+ expect(intervalFn).toContain('function test:__interval_body_0');
98
+ expect(intervalFn).toContain('schedule function test:__interval_0 20t');
99
+ expect(intervalBodyFn).toContain('say tick');
100
+ expect(timeoutFn).toContain('say later');
101
+ });
102
+ });
103
+ describe('is type narrowing', () => {
104
+ it('type checks and compiles entity narrowing inside foreach blocks', () => {
105
+ const source = `
106
+ fn main() {
107
+ foreach (e in @e) {
108
+ if (e is Player) {
109
+ kill(e);
110
+ }
111
+ if (e is Zombie) {
112
+ kill(e);
113
+ }
114
+ }
115
+ }
116
+ `;
117
+ expect(typeCheck(source)).toEqual([]);
118
+ const files = compile(source);
119
+ const mainFn = getFunction(files, 'main');
120
+ const foreachFn = getSubFunction(files, 'main', 'foreach_0');
121
+ const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'));
122
+ expect(mainFn).toContain('execute as @e run function test:main/foreach_0');
123
+ expect(foreachFn).toContain('execute if entity @s[type=minecraft:player] run function test:main/then_');
124
+ expect(foreachFn).toContain('execute if entity @s[type=minecraft:zombie] run function test:main/then_');
125
+ expect(thenFiles).toHaveLength(2);
126
+ });
127
+ });
128
+ describe('impl blocks', () => {
129
+ it('compiles static and instance impl methods end to end', () => {
130
+ const source = `
131
+ struct Point { x: int, y: int }
132
+
133
+ impl Point {
134
+ fn new(x: int, y: int) -> Point {
135
+ return { x: x, y: y };
136
+ }
137
+
138
+ fn distance(self) -> int {
139
+ return self.x + self.y;
140
+ }
141
+ }
142
+
143
+ fn main() {
144
+ let p: Point = Point::new(1, 2);
145
+ let d: int = p.distance();
146
+ say("\${d}");
147
+ }
148
+ `;
149
+ expect(typeCheck(source)).toEqual([]);
150
+ const files = compile(source);
151
+ const mainFn = getFunction(files, 'main');
152
+ const staticFn = getFunction(files, 'Point_new');
153
+ const instanceFn = getFunction(files, 'Point_distance');
154
+ expect(mainFn).toContain('function test:Point_new');
155
+ expect(mainFn).toContain('function test:Point_distance');
156
+ expect(staticFn).toBeDefined();
157
+ expect(instanceFn).toBeDefined();
158
+ });
159
+ });
160
+ describe('namespace prefixing', () => {
161
+ it('prefixes user objectives but preserves mc_name and qualified objectives', () => {
162
+ const files = compile(`
163
+ fn main() {
164
+ scoreboard_set("timer", #rs, 100);
165
+ scoreboard_set(@s, "timer", 100);
166
+ scoreboard_set(@s, #health, 20);
167
+ scoreboard_set(@s, "custom.timer", 1);
168
+ }
169
+ `, 'pack');
170
+ const mainFn = getFunction(files, 'main');
171
+ expect(mainFn).toContain('scoreboard players set timer rs 100');
172
+ expect(mainFn).toContain('scoreboard players set @s pack.timer 100');
173
+ expect(mainFn).toContain('scoreboard players set @s health 20');
174
+ expect(mainFn).toContain('scoreboard players set @s custom.timer 1');
175
+ });
176
+ });
177
+ describe('Timer OOP API', () => {
178
+ it('compiles the Timer impl API end to end', () => {
179
+ const source = `
180
+ struct Timer {
181
+ _id: int,
182
+ _duration: int
183
+ }
184
+
185
+ impl Timer {
186
+ fn new(duration: int) -> Timer {
187
+ scoreboard_set("timer_ticks", #rs, 0);
188
+ scoreboard_set("timer_active", #rs, 0);
189
+ return { _id: 0, _duration: duration };
190
+ }
191
+
192
+ fn start(self) {
193
+ scoreboard_set("timer_active", #rs, 1);
194
+ }
195
+
196
+ fn pause(self) {
197
+ scoreboard_set("timer_active", #rs, 0);
198
+ }
199
+
200
+ fn reset(self) {
201
+ scoreboard_set("timer_ticks", #rs, 0);
202
+ }
203
+
204
+ fn done(self) -> bool {
205
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
206
+ return ticks >= self._duration;
207
+ }
208
+
209
+ fn tick(self) {
210
+ let active: int = scoreboard_get("timer_active", #rs);
211
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
212
+
213
+ if (active == 1) {
214
+ if (ticks < self._duration) {
215
+ scoreboard_set("timer_ticks", #rs, ticks + 1);
216
+ }
217
+ }
218
+ }
219
+ }
220
+
221
+ fn main() {
222
+ let t: Timer = Timer::new(100);
223
+ t.start();
224
+ t.tick();
225
+ let finished: bool = t.done();
226
+ if (finished) {
227
+ say("done");
228
+ }
229
+ t.pause();
230
+ t.reset();
231
+ }
232
+ `;
233
+ expect(typeCheck(source)).toEqual([]);
234
+ const files = compile(source, 'timerapi');
235
+ const mainFn = getFunction(files, 'main');
236
+ const newFn = getFunction(files, 'Timer_new');
237
+ const startFn = getFunction(files, 'Timer_start');
238
+ const tickFn = getFunction(files, 'Timer_tick');
239
+ const doneFn = getFunction(files, 'Timer_done');
240
+ const pauseFn = getFunction(files, 'Timer_pause');
241
+ const resetFn = getFunction(files, 'Timer_reset');
242
+ expect(mainFn).toContain('function timerapi:Timer_new');
243
+ expect(mainFn).toContain('function timerapi:Timer_start');
244
+ expect(mainFn).toContain('function timerapi:Timer_tick');
245
+ expect(mainFn).toContain('function timerapi:Timer_done');
246
+ expect(newFn).toContain('scoreboard players set timer_ticks rs 0');
247
+ expect(startFn).toContain('scoreboard players set timer_active rs 1');
248
+ expect(tickFn).toContain('scoreboard players get timer_active rs');
249
+ expect(doneFn).toContain('scoreboard players get timer_ticks rs');
250
+ expect(pauseFn).toContain('scoreboard players set timer_active rs 0');
251
+ expect(resetFn).toContain('scoreboard players set timer_ticks rs 0');
252
+ });
253
+ });
254
+ describe('advancement event decorators', () => {
255
+ it('generates advancement json with reward function path', () => {
256
+ const source = `
257
+ @on_advancement("story/mine_diamond")
258
+ fn on_mine_diamond() {
259
+ title(@s, "Diamond");
260
+ }
261
+ `;
262
+ const files = compile(source);
263
+ const advancement = files.find(f => f.path === 'data/test/advancements/on_advancement_on_mine_diamond.json');
264
+ expect(advancement).toBeDefined();
265
+ const content = JSON.parse(advancement.content);
266
+ expect(content.criteria.trigger.trigger).toBe('minecraft:story/mine_diamond');
267
+ expect(content.rewards.function).toBe('test:on_mine_diamond');
268
+ });
269
+ });
270
+ describe('Test 1: Simple function (add)', () => {
271
+ const source = `
272
+ fn add(a: int, b: int) -> int {
273
+ return a + b;
274
+ }
275
+ `;
276
+ it('generates mcfunction file', () => {
277
+ const files = compile(source);
278
+ const fn = getFunction(files, 'add');
279
+ expect(fn).toBeDefined();
280
+ });
281
+ it('copies params to named variables', () => {
282
+ const files = compile(source);
283
+ const fn = getFunction(files, 'add');
284
+ expect(fn).toContain('$a');
285
+ expect(fn).toContain('$p0');
286
+ });
287
+ it('performs addition', () => {
288
+ const files = compile(source);
289
+ const fn = getFunction(files, 'add');
290
+ expect(fn).toContain('+=');
291
+ });
292
+ it('returns result', () => {
293
+ const files = compile(source);
294
+ const fn = getFunction(files, 'add');
295
+ expect(fn).toMatch(/return/);
296
+ });
297
+ });
298
+ describe('Test 2: if/else (abs)', () => {
299
+ const source = `
300
+ fn abs(x: int) -> int {
301
+ if (x < 0) {
302
+ return -x;
303
+ } else {
304
+ return x;
305
+ }
306
+ }
307
+ `;
308
+ it('generates main function and control flow blocks', () => {
309
+ const files = compile(source);
310
+ const fn = getFunction(files, 'abs');
311
+ expect(fn).toBeDefined();
312
+ // Should have conditional execution
313
+ expect(fn).toContain('execute if score');
314
+ });
315
+ it('has comparison logic', () => {
316
+ const files = compile(source);
317
+ const fn = getFunction(files, 'abs');
318
+ // Check for comparison with 0
319
+ expect(fn).toContain('$const_0');
320
+ });
321
+ });
322
+ describe('Test 3: @tick + say', () => {
323
+ const source = `
324
+ @tick(rate=20)
325
+ fn heartbeat() {
326
+ say("still alive");
327
+ }
328
+ `;
329
+ it('generates function with say command', () => {
330
+ const files = compile(source);
331
+ // Find the tick_body or main function that has the say command
332
+ const allContent = files.map(f => f.content).join('\n');
333
+ expect(allContent).toContain('say still alive');
334
+ });
335
+ it('is registered in tick tag', () => {
336
+ const files = compile(source);
337
+ expect(hasTickTag(files, 'test', 'heartbeat')).toBe(true);
338
+ });
339
+ });
340
+ describe('Builtins: command emission', () => {
341
+ it('compiles UI and broadcast builtins', () => {
342
+ const source = `
343
+ fn test() {
344
+ actionbar(@a, "Fight!");
345
+ subtitle(@a, "Wave 2");
346
+ title_times(@a, 10, 60, 10);
347
+ announce("Arena live");
348
+ }
349
+ `;
350
+ const fn = getFunction(compile(source), 'test');
351
+ expect(fn).toContain('title @a actionbar {"text":"Fight!"}');
352
+ expect(fn).toContain('title @a subtitle {"text":"Wave 2"}');
353
+ expect(fn).toContain('title @a times 10 60 10');
354
+ expect(fn).toContain('tellraw @a {"text":"Arena live"}');
355
+ });
356
+ it('compiles world and utility builtins', () => {
357
+ const source = `
358
+ fn test() {
359
+ tp(@s, (~1, ~0, ~-1));
360
+ tp(@s, @p);
361
+ tp(@a, (1, 64, 1));
362
+ clear(@s);
363
+ weather("clear");
364
+ time_set("noon");
365
+ gamerule("doWeatherCycle", "false");
366
+ setblock((0, 64, 0), "stone");
367
+ fill((0, 64, 0), (2, 66, 2), "glass");
368
+ clone((0, 64, 0), (2, 66, 2), (10, 64, 10));
369
+ xp_add(@s, 5);
370
+ xp_set(@s, 1, "levels");
371
+ }
372
+ `;
373
+ const fn = getFunction(compile(source), 'test');
374
+ expect(fn).toContain('tp @s ~1 ~ ~-1');
375
+ expect(fn).toContain('tp @s @p');
376
+ expect(fn).toContain('tp @a 1 64 1');
377
+ expect(fn).toContain('clear @s');
378
+ expect(fn).toContain('weather clear');
379
+ expect(fn).toContain('time set noon');
380
+ expect(fn).toContain('gamerule doWeatherCycle false');
381
+ expect(fn).toContain('setblock 0 64 0 stone');
382
+ expect(fn).toContain('fill 0 64 0 2 66 2 glass');
383
+ expect(fn).toContain('clone 0 64 0 2 66 2 10 64 10');
384
+ expect(fn).toContain('xp add @s 5 points');
385
+ expect(fn).toContain('xp set @s 1 levels');
386
+ });
387
+ it('compiles scoreboard display and objective builtins', () => {
388
+ const source = `
389
+ fn test() {
390
+ scoreboard_display("sidebar", "kills");
391
+ scoreboard_display("list", "coins");
392
+ scoreboard_display("belowName", "hp");
393
+ scoreboard_hide("sidebar");
394
+ scoreboard_add_objective("kills", "playerKillCount", "Kill Count");
395
+ scoreboard_remove_objective("kills");
396
+ }
397
+ `;
398
+ const fn = getFunction(compile(source), 'test');
399
+ expect(fn).toContain('scoreboard objectives setdisplay sidebar test.kills');
400
+ expect(fn).toContain('scoreboard objectives setdisplay list test.coins');
401
+ expect(fn).toContain('scoreboard objectives setdisplay belowName test.hp');
402
+ expect(fn).toContain('scoreboard objectives setdisplay sidebar');
403
+ expect(fn).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
404
+ expect(fn).toContain('scoreboard objectives remove test.kills');
405
+ });
406
+ it('compiles bossbar builtins', () => {
407
+ const source = `
408
+ fn test() {
409
+ bossbar_add("ns:health", "Boss Health");
410
+ bossbar_set_value("ns:health", 50);
411
+ bossbar_set_max("ns:health", 100);
412
+ bossbar_set_color("ns:health", "red");
413
+ bossbar_set_style("ns:health", "notched_10");
414
+ bossbar_set_visible("ns:health", true);
415
+ bossbar_set_players("ns:health", @a);
416
+ bossbar_remove("ns:health");
417
+ let current: int = bossbar_get_value("ns:health");
418
+ }
419
+ `;
420
+ const fn = getFunction(compile(source), 'test');
421
+ expect(fn).toContain('bossbar add ns:health {"text":"Boss Health"}');
422
+ expect(fn).toContain('bossbar set ns:health value 50');
423
+ expect(fn).toContain('bossbar set ns:health max 100');
424
+ expect(fn).toContain('bossbar set ns:health color red');
425
+ expect(fn).toContain('bossbar set ns:health style notched_10');
426
+ expect(fn).toContain('bossbar set ns:health visible true');
427
+ expect(fn).toContain('bossbar set ns:health players @a');
428
+ expect(fn).toContain('bossbar remove ns:health');
429
+ expect(fn).toMatch(/execute store result score \$_\d+ rs run bossbar get ns:health value/);
430
+ });
431
+ it('compiles team builtins', () => {
432
+ const source = `
433
+ fn test() {
434
+ team_add("red", "Red Team");
435
+ team_remove("red");
436
+ team_join("red", @a[tag=red_team]);
437
+ team_leave(@s);
438
+ team_option("red", "friendlyFire", "false");
439
+ team_option("red", "color", "red");
440
+ team_option("red", "prefix", "[Red] ");
441
+ }
442
+ `;
443
+ const fn = getFunction(compile(source), 'test');
444
+ expect(fn).toContain('team add red {"text":"Red Team"}');
445
+ expect(fn).toContain('team remove red');
446
+ expect(fn).toContain('team join red @a[tag=red_team]');
447
+ expect(fn).toContain('team leave @s');
448
+ expect(fn).toContain('team modify red friendlyFire false');
449
+ expect(fn).toContain('team modify red color red');
450
+ expect(fn).toContain('team modify red prefix {"text":"[Red] "}');
451
+ });
452
+ });
453
+ describe('Test 4: foreach', () => {
454
+ const source = `
455
+ fn kill_zombies() {
456
+ foreach (z in @e[type=zombie, distance=..10]) {
457
+ kill(z);
458
+ }
459
+ }
460
+ `;
461
+ it('generates main function with execute as', () => {
462
+ const files = compile(source);
463
+ const fn = getFunction(files, 'kill_zombies');
464
+ expect(fn).toBeDefined();
465
+ expect(fn).toContain('execute as @e[type=minecraft:zombie,distance=..10]');
466
+ expect(fn).toContain('run function test:kill_zombies/foreach_0');
467
+ });
468
+ it('generates sub-function with kill @s', () => {
469
+ const files = compile(source);
470
+ // Look for the foreach sub-function
471
+ const subFn = files.find(f => f.path.includes('foreach_0'));
472
+ expect(subFn).toBeDefined();
473
+ expect(subFn?.content).toContain('kill @s');
474
+ });
475
+ });
476
+ describe('Test 5: while loop (countdown)', () => {
477
+ const source = `
478
+ fn count_down() {
479
+ let i: int = 10;
480
+ while (i > 0) {
481
+ i = i - 1;
482
+ }
483
+ }
484
+ `;
485
+ it('generates function with loop structure', () => {
486
+ const files = compile(source);
487
+ const fn = getFunction(files, 'count_down');
488
+ expect(fn).toBeDefined();
489
+ });
490
+ it('initializes variable to 10', () => {
491
+ const files = compile(source);
492
+ const fn = getFunction(files, 'count_down');
493
+ expect(fn).toContain('10');
494
+ });
495
+ it('has comparison and conditional jumps', () => {
496
+ const files = compile(source);
497
+ const allContent = files
498
+ .filter(f => f.path.includes('count_down'))
499
+ .map(f => f.content)
500
+ .join('\n');
501
+ // Should have comparison with 0
502
+ expect(allContent).toContain('$const_0');
503
+ // Should have conditional execution
504
+ expect(allContent).toMatch(/execute if score/);
505
+ });
506
+ });
507
+ describe('Test 6: arrays', () => {
508
+ const source = `
509
+ fn arrays() {
510
+ let arr: int[] = [1, 2, 3];
511
+ let first: int = arr[0];
512
+ let i: int = 1;
513
+ let second: int = arr[i];
514
+ let len: int = arr.len;
515
+ arr.push(4);
516
+ let last: int = arr.pop();
517
+ foreach (x in arr) {
518
+ say("loop");
519
+ }
520
+ }
521
+ `;
522
+ it('generates array storage commands', () => {
523
+ const files = compile(source);
524
+ const fn = getFunction(files, 'arrays');
525
+ expect(fn).toBeDefined();
526
+ expect(fn).toContain('data modify storage rs:heap arr set value []');
527
+ expect(fn).toContain('data modify storage rs:heap arr append value 1');
528
+ expect(fn).toContain('data modify storage rs:heap arr append value 4');
529
+ expect(fn).toContain('data remove storage rs:heap arr[-1]');
530
+ });
531
+ it('generates array access helpers', () => {
532
+ const files = compile(source);
533
+ const fn = getFunction(files, 'arrays');
534
+ expect(fn).toContain('run data get storage rs:heap arr[0]');
535
+ expect(fn).toContain('with storage rs:heap');
536
+ const helper = files.find(f => f.path.includes('array_get_'));
537
+ expect(helper?.content).toContain('arr[$(');
538
+ });
539
+ it('generates array foreach loop', () => {
540
+ const files = compile(source);
541
+ const allContent = files
542
+ .filter(f => f.path.includes('/arrays'))
543
+ .map(f => f.content)
544
+ .join('\n');
545
+ expect(allContent).toContain('foreach_array_check');
546
+ expect(allContent).toContain('say loop');
547
+ });
548
+ });
549
+ describe('Datapack structure', () => {
550
+ it('generates pack.mcmeta with proper format and description', () => {
551
+ const files = compile('fn test() {}');
552
+ const meta = files.find(f => f.path === 'pack.mcmeta');
553
+ expect(meta).toBeDefined();
554
+ const content = JSON.parse(meta.content);
555
+ expect(content.pack.pack_format).toBe(26);
556
+ expect(content.pack.description).toBe('test datapack — compiled by redscript');
557
+ });
558
+ it('generates __load.mcfunction with scoreboard setup', () => {
559
+ const files = compile('fn test() {}');
560
+ const load = files.find(f => f.path.includes('__load.mcfunction'));
561
+ expect(load).toBeDefined();
562
+ expect(load.content).toContain('# RedScript runtime init');
563
+ expect(load.content).toContain('scoreboard objectives add rs dummy');
564
+ });
565
+ it('generates minecraft:load tag pointing to __load', () => {
566
+ const files = compile('fn test() {}');
567
+ const tag = files.find(f => f.path === 'data/minecraft/tags/function/load.json');
568
+ expect(tag).toBeDefined();
569
+ const content = JSON.parse(tag.content);
570
+ expect(content.values).toContain('test:__load');
571
+ });
572
+ it('generates __tick.mcfunction for tick functions', () => {
573
+ const source = '@tick fn tick_fn() { say("tick"); }';
574
+ const files = compile(source);
575
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
576
+ expect(tickFn).toBeDefined();
577
+ expect(tickFn.content).toContain('# RedScript tick dispatcher');
578
+ expect(tickFn.content).toContain('function test:tick_fn');
579
+ });
580
+ it('generates minecraft:tick tag pointing to __tick', () => {
581
+ const source = '@tick fn tick_fn() { say("tick"); }';
582
+ const files = compile(source);
583
+ const tag = files.find(f => f.path === 'data/minecraft/tags/function/tick.json');
584
+ expect(tag).toBeDefined();
585
+ const content = JSON.parse(tag.content);
586
+ expect(content.values).toContain('test:__tick');
587
+ });
588
+ it('does not generate tick infrastructure when no tick functions', () => {
589
+ const files = compile('fn test() {}');
590
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
591
+ const tickTag = files.find(f => f.path.includes('tick.json'));
592
+ expect(tickFn).toBeUndefined();
593
+ expect(tickTag).toBeUndefined();
594
+ });
595
+ });
596
+ describe('Scoreboard interop', () => {
597
+ it('compiles scoreboard_get to read vanilla scores', () => {
598
+ const source = `
599
+ fn test() -> int {
600
+ let kills: int = scoreboard_get("PlayerName", "kill_count");
601
+ return kills;
602
+ }
603
+ `;
604
+ const files = compile(source);
605
+ const fn = getFunction(files, 'test');
606
+ expect(fn).toBeDefined();
607
+ expect(fn).toContain('execute store result score');
608
+ expect(fn).toContain('scoreboard players get PlayerName test.kill_count');
609
+ });
610
+ it('compiles scoreboard_get with @s selector', () => {
611
+ const source = `
612
+ fn test() -> int {
613
+ let my_kills: int = scoreboard_get("@s", "kill_count");
614
+ return my_kills;
615
+ }
616
+ `;
617
+ const files = compile(source);
618
+ const fn = getFunction(files, 'test');
619
+ expect(fn).toBeDefined();
620
+ expect(fn).toContain('scoreboard players get @s test.kill_count');
621
+ });
622
+ it('compiles scoreboard_set with constant value', () => {
623
+ const source = `
624
+ fn test() {
625
+ scoreboard_set("PlayerName", "kill_count", 100);
626
+ }
627
+ `;
628
+ const files = compile(source);
629
+ const fn = getFunction(files, 'test');
630
+ expect(fn).toBeDefined();
631
+ expect(fn).toContain('scoreboard players set PlayerName test.kill_count 100');
632
+ });
633
+ it('compiles scoreboard_set with variable value', () => {
634
+ const source = `
635
+ fn test() {
636
+ let value: int = 42;
637
+ scoreboard_set("@s", "score", value);
638
+ }
639
+ `;
640
+ const files = compile(source);
641
+ const allContent = files
642
+ .filter(f => f.path.includes('test'))
643
+ .map(f => f.content)
644
+ .join('\n');
645
+ expect(allContent).toContain('execute store result score @s test.score');
646
+ });
647
+ it('compiles score() as expression', () => {
648
+ const source = `
649
+ fn test() -> int {
650
+ let level: int = score("@s", "minecraft.custom:minecraft.play_one_minute");
651
+ return level;
652
+ }
653
+ `;
654
+ const files = compile(source);
655
+ const fn = getFunction(files, 'test');
656
+ expect(fn).toBeDefined();
657
+ expect(fn).toContain('scoreboard players get @s minecraft.custom:minecraft.play_one_minute');
658
+ });
659
+ it('uses scoreboard values in expressions', () => {
660
+ const source = `
661
+ fn double_score() -> int {
662
+ let s: int = scoreboard_get("@s", "points");
663
+ let doubled: int = s * 2;
664
+ scoreboard_set("@s", "points", doubled);
665
+ return doubled;
666
+ }
667
+ `;
668
+ const files = compile(source);
669
+ const fn = getFunction(files, 'double_score');
670
+ expect(fn).toBeDefined();
671
+ expect(fn).toContain('scoreboard players get @s test.points');
672
+ });
673
+ });
674
+ describe('Built-in functions', () => {
675
+ it('compiles give()', () => {
676
+ const source = 'fn test() { give(@p, "diamond", 64); }';
677
+ const files = compile(source);
678
+ const fn = getFunction(files, 'test');
679
+ expect(fn).toContain('give @p diamond 64');
680
+ });
681
+ it('compiles summon()', () => {
682
+ const source = 'fn test() { summon("zombie"); }';
683
+ const files = compile(source);
684
+ const fn = getFunction(files, 'test');
685
+ expect(fn).toContain('summon zombie');
686
+ });
687
+ it('compiles effect()', () => {
688
+ const source = 'fn test() { effect(@a, "speed", 60, 2); }';
689
+ const files = compile(source);
690
+ const fn = getFunction(files, 'test');
691
+ expect(fn).toContain('effect give @a speed 60 2');
692
+ });
693
+ it('compiles tp()', () => {
694
+ const source = 'fn test() { tp(@s, (0, 100, 0)); }';
695
+ const files = compile(source);
696
+ const fn = getFunction(files, 'test');
697
+ expect(fn).toContain('tp @s 0 100 0');
698
+ });
699
+ it('type checks tp selector destinations', () => {
700
+ const invalid = typeCheck('fn test() { tp(@s, @a); }');
701
+ expect(invalid.map(err => err.message)).toContain('tp destination must be a single-entity selector (@s, @p, @r, or limit=1)');
702
+ expect(typeCheck('fn test() { tp(@s, @p); }')).toHaveLength(0);
703
+ expect(typeCheck('fn test() { tp(@s, @e[limit=1, tag=target]); }')).toHaveLength(0);
704
+ });
705
+ it('compiles random()', () => {
706
+ const source = 'fn test() { let x: int = random(1, 10); }';
707
+ const files = compile(source);
708
+ const fn = getFunction(files, 'test');
709
+ expect(fn).toContain('scoreboard players random $_0 rs 1 10');
710
+ });
711
+ it('compiles random_native()', () => {
712
+ const source = 'fn test() { let x: int = random_native(1, 6); }';
713
+ const files = compile(source);
714
+ const fn = getFunction(files, 'test');
715
+ expect(fn).toContain('execute store result score $_0 rs run random value 1 6');
716
+ });
717
+ it('compiles random_native() with zero min', () => {
718
+ const source = 'fn test() { let x: int = random_native(0, 100); }';
719
+ const files = compile(source);
720
+ const fn = getFunction(files, 'test');
721
+ expect(fn).toContain('execute store result score $_0 rs run random value 0 100');
722
+ });
723
+ it('compiles random_sequence()', () => {
724
+ const source = 'fn test() { random_sequence("loot"); random_sequence("loot", 9); }';
725
+ const files = compile(source);
726
+ const fn = getFunction(files, 'test');
727
+ expect(fn).toContain('random reset loot 0');
728
+ expect(fn).toContain('random reset loot 9');
729
+ });
730
+ });
731
+ describe('Selectors', () => {
732
+ it('handles simple selectors', () => {
733
+ const source = 'fn test() { kill(@e); }';
734
+ const files = compile(source);
735
+ const fn = getFunction(files, 'test');
736
+ expect(fn).toContain('kill @e');
737
+ });
738
+ it('handles selectors with type filter', () => {
739
+ const source = 'fn test() { kill(@e[type=creeper]); }';
740
+ const files = compile(source);
741
+ const fn = getFunction(files, 'test');
742
+ expect(fn).toContain('kill @e[type=minecraft:creeper]');
743
+ });
744
+ it('handles selectors with multiple filters', () => {
745
+ const source = 'fn test() { kill(@e[type=zombie, distance=..5, limit=1]); }';
746
+ const files = compile(source);
747
+ const fn = getFunction(files, 'test');
748
+ expect(fn).toContain('type=minecraft:zombie');
749
+ expect(fn).toContain('distance=..5');
750
+ expect(fn).toContain('limit=1');
751
+ });
752
+ it('handles tag filters', () => {
753
+ const source = 'fn test() { kill(@e[tag=boss, tag=!friendly]); }';
754
+ const files = compile(source);
755
+ const fn = getFunction(files, 'test');
756
+ expect(fn).toContain('tag=boss');
757
+ expect(fn).toContain('tag=!friendly');
758
+ });
759
+ });
760
+ describe('For loop', () => {
761
+ it('compiles basic for loop', () => {
762
+ const source = `
763
+ fn count() {
764
+ for (let i: int = 0; i < 10; i = i + 1) {
765
+ say("loop");
766
+ }
767
+ }
768
+ `;
769
+ const files = compile(source);
770
+ const allContent = files
771
+ .filter(f => f.path.includes('count'))
772
+ .map(f => f.content)
773
+ .join('\n');
774
+ // Should have initialization (set to 0)
775
+ expect(allContent).toContain('0');
776
+ // Should have say command
777
+ expect(allContent).toContain('say loop');
778
+ // Should have comparison
779
+ expect(allContent).toContain('$const_10');
780
+ // Should have loop structure
781
+ expect(allContent).toContain('for_check');
782
+ });
783
+ it('compiles for loop without init', () => {
784
+ const source = `
785
+ fn count() {
786
+ let i: int = 5;
787
+ for (; i > 0; i = i - 1) {
788
+ say("counting");
789
+ }
790
+ }
791
+ `;
792
+ const files = compile(source);
793
+ const allContent = files
794
+ .filter(f => f.path.includes('count'))
795
+ .map(f => f.content)
796
+ .join('\n');
797
+ expect(allContent).toContain('say counting');
798
+ expect(allContent).toContain('for_check');
799
+ });
800
+ it('compiles for loop with compound step', () => {
801
+ const source = `
802
+ fn double() {
803
+ for (let x: int = 1; x < 100; x = x * 2) {
804
+ say("doubling");
805
+ }
806
+ }
807
+ `;
808
+ const files = compile(source);
809
+ const fn = getFunction(files, 'double');
810
+ expect(fn).toBeDefined();
811
+ });
812
+ it('generates correct control flow blocks', () => {
813
+ const source = `
814
+ fn loop_test() {
815
+ for (let i: int = 0; i < 5; i = i + 1) {
816
+ say("iteration");
817
+ }
818
+ }
819
+ `;
820
+ const files = compile(source);
821
+ // Should have for_check block
822
+ const checkBlock = files.find(f => f.path.includes('for_check'));
823
+ expect(checkBlock).toBeDefined();
824
+ // Should have for_body block
825
+ const bodyBlock = files.find(f => f.path.includes('for_body'));
826
+ expect(bodyBlock).toBeDefined();
827
+ });
828
+ it('compiles nested for loops', () => {
829
+ const source = `
830
+ fn nested() {
831
+ for (let i: int = 0; i < 3; i = i + 1) {
832
+ for (let j: int = 0; j < 3; j = j + 1) {
833
+ say("nested");
834
+ }
835
+ }
836
+ }
837
+ `;
838
+ const files = compile(source);
839
+ const allContent = files
840
+ .filter(f => f.path.includes('nested'))
841
+ .map(f => f.content)
842
+ .join('\n');
843
+ expect(allContent).toContain('say nested');
844
+ });
845
+ });
846
+ describe('Control flow', () => {
847
+ it('handles nested if statements', () => {
848
+ const source = `
849
+ fn nested(x: int, y: int) {
850
+ if (x > 0) {
851
+ if (y > 0) {
852
+ say("both positive");
853
+ }
854
+ }
855
+ }
856
+ `;
857
+ const files = compile(source);
858
+ const allContent = files
859
+ .filter(f => f.path.includes('nested'))
860
+ .map(f => f.content)
861
+ .join('\n');
862
+ expect(allContent).toContain('say both positive');
863
+ });
864
+ it('handles else-if chains', () => {
865
+ const source = `
866
+ fn grade(score: int) {
867
+ if (score >= 90) {
868
+ say("A");
869
+ } else {
870
+ if (score >= 80) {
871
+ say("B");
872
+ } else {
873
+ say("C");
874
+ }
875
+ }
876
+ }
877
+ `;
878
+ const files = compile(source);
879
+ const allContent = files
880
+ .filter(f => f.path.includes('grade'))
881
+ .map(f => f.content)
882
+ .join('\n');
883
+ expect(allContent).toContain('say A');
884
+ expect(allContent).toContain('say B');
885
+ expect(allContent).toContain('say C');
886
+ });
887
+ });
888
+ describe('as/at blocks', () => {
889
+ it('compiles as block', () => {
890
+ const source = `
891
+ fn greet_all() {
892
+ as @a {
893
+ say("Hello!");
894
+ }
895
+ }
896
+ `;
897
+ const files = compile(source);
898
+ const fn = getFunction(files, 'greet_all');
899
+ expect(fn).toContain('execute as @a');
900
+ expect(fn).toContain('run function test:greet_all/');
901
+ });
902
+ it('compiles at block', () => {
903
+ const source = `
904
+ fn spawn_at_players() {
905
+ at @a {
906
+ summon("zombie");
907
+ }
908
+ }
909
+ `;
910
+ const files = compile(source);
911
+ const fn = getFunction(files, 'spawn_at_players');
912
+ expect(fn).toContain('execute at @a');
913
+ expect(fn).toContain('run function test:spawn_at_players/');
914
+ });
915
+ });
916
+ describe('Float type (fixed-point)', () => {
917
+ it('stores float as fixed-point × 1000', () => {
918
+ const source = `
919
+ fn test() -> int {
920
+ let pi: float = 3.14;
921
+ return pi;
922
+ }
923
+ `;
924
+ const files = compile(source);
925
+ const allContent = files.map(f => f.content).join('\n');
926
+ // 3.14 * 1000 = 3140
927
+ expect(allContent).toContain('3140');
928
+ });
929
+ it('handles float addition correctly', () => {
930
+ const source = `
931
+ fn test() -> int {
932
+ let a: float = 1.5;
933
+ let b: float = 2.5;
934
+ let c: float = a + b;
935
+ return c;
936
+ }
937
+ `;
938
+ const files = compile(source);
939
+ const allContent = files.map(f => f.content).join('\n');
940
+ // 1.5 * 1000 = 1500, 2.5 * 1000 = 2500
941
+ expect(allContent).toContain('1500');
942
+ expect(allContent).toContain('2500');
943
+ // Addition should use +=
944
+ expect(allContent).toContain('+=');
945
+ });
946
+ it('handles float multiplication with scaling', () => {
947
+ const source = `
948
+ fn test() {
949
+ let a: float = 2.0;
950
+ let b: float = 3.0;
951
+ let c: float = a * b;
952
+ }
953
+ `;
954
+ const files = compile(source);
955
+ const allContent = files.map(f => f.content).join('\n');
956
+ // Should have 2000 and 3000 (the fixed-point values)
957
+ expect(allContent).toContain('2000');
958
+ expect(allContent).toContain('3000');
959
+ // Should divide after multiplication (for fixed-point correction)
960
+ expect(allContent).toContain('/=');
961
+ });
962
+ it('handles float division with scaling', () => {
963
+ const source = `
964
+ fn test() {
965
+ let a: float = 10.0;
966
+ let b: float = 2.0;
967
+ let c: float = a / b;
968
+ }
969
+ `;
970
+ const files = compile(source);
971
+ const allContent = files.map(f => f.content).join('\n');
972
+ // Should multiply by 1000 before division
973
+ expect(allContent).toContain('*=');
974
+ // Should then divide
975
+ expect(allContent).toContain('/=');
976
+ });
977
+ it('handles small float literals', () => {
978
+ const source = `
979
+ fn test() -> int {
980
+ let x: float = 0.001;
981
+ let y: float = 0.5;
982
+ let z: float = x + y;
983
+ return 0;
984
+ }
985
+ `;
986
+ const files = compile(source);
987
+ const allContent = files.map(f => f.content).join('\n');
988
+ // 0.001 * 1000 = 1, 0.5 * 1000 = 500
989
+ // Check that fixed-point values are present
990
+ expect(allContent).toContain('$const_1');
991
+ expect(allContent).toContain('500');
992
+ });
993
+ it('handles float in expressions', () => {
994
+ const source = `
995
+ fn calc() {
996
+ let speed: float = 1.5;
997
+ let time: float = 2.0;
998
+ let distance: float = speed * time;
999
+ }
1000
+ `;
1001
+ const files = compile(source);
1002
+ const fn = getFunction(files, 'calc');
1003
+ expect(fn).toBeDefined();
1004
+ });
1005
+ });
1006
+ describe('Optimization', () => {
1007
+ it('folds constants', () => {
1008
+ const source = 'fn test() -> int { return 2 + 3; }';
1009
+ const files = compile(source);
1010
+ const fn = getFunction(files, 'test');
1011
+ // After constant folding, should have direct value 5
1012
+ expect(fn).toContain('5');
1013
+ });
1014
+ it('propagates copies', () => {
1015
+ const source = `
1016
+ fn test() -> int {
1017
+ let x: int = 10;
1018
+ let y: int = x;
1019
+ return y;
1020
+ }
1021
+ `;
1022
+ const files = compile(source);
1023
+ const fn = getFunction(files, 'test');
1024
+ // Should have 10 in the output (propagated)
1025
+ expect(fn).toContain('10');
1026
+ });
1027
+ });
1028
+ describe('Multiple functions', () => {
1029
+ it('compiles multiple functions', () => {
1030
+ const source = `
1031
+ fn helper() -> int {
1032
+ return 42;
1033
+ }
1034
+
1035
+ fn main() -> int {
1036
+ return helper();
1037
+ }
1038
+ `;
1039
+ const files = compile(source);
1040
+ expect(getFunction(files, 'helper')).toBeDefined();
1041
+ expect(getFunction(files, 'main')).toBeDefined();
1042
+ });
1043
+ it('generates function calls', () => {
1044
+ const source = `
1045
+ fn helper() -> int {
1046
+ return 42;
1047
+ }
1048
+
1049
+ fn main() -> int {
1050
+ return helper();
1051
+ }
1052
+ `;
1053
+ const files = compile(source);
1054
+ const main = getFunction(files, 'main');
1055
+ expect(main).toContain('function test:helper');
1056
+ });
1057
+ });
1058
+ describe('Raw commands', () => {
1059
+ it('passes through raw commands', () => {
1060
+ const source = 'fn test() { raw("gamemode creative @a"); }';
1061
+ const files = compile(source);
1062
+ const fn = getFunction(files, 'test');
1063
+ expect(fn).toContain('gamemode creative @a');
1064
+ });
1065
+ it('preserves complex raw commands', () => {
1066
+ const source = 'fn test() { raw("execute as @a at @s run particle flame ~ ~ ~ 0.5 0.5 0.5 0 10"); }';
1067
+ const files = compile(source);
1068
+ const fn = getFunction(files, 'test');
1069
+ expect(fn).toContain('execute as @a at @s run particle flame');
1070
+ });
1071
+ });
1072
+ describe('Compound assignment', () => {
1073
+ it('compiles += operator', () => {
1074
+ const source = `
1075
+ fn test() {
1076
+ let x: int = 5;
1077
+ x += 3;
1078
+ }
1079
+ `;
1080
+ const files = compile(source);
1081
+ const allContent = files.map(f => f.content).join('\n');
1082
+ // Should have both 5 and 3, and addition
1083
+ expect(allContent).toContain('5');
1084
+ expect(allContent).toContain('3');
1085
+ });
1086
+ it('compiles all compound operators', () => {
1087
+ const source = `
1088
+ fn test() {
1089
+ let x: int = 10;
1090
+ x += 1;
1091
+ x -= 1;
1092
+ x *= 2;
1093
+ x /= 2;
1094
+ x %= 3;
1095
+ }
1096
+ `;
1097
+ const files = compile(source);
1098
+ // Should compile without error
1099
+ expect(getFunction(files, 'test')).toBeDefined();
1100
+ });
1101
+ });
1102
+ describe('Trigger system', () => {
1103
+ it('generates trigger objective in __load.mcfunction', () => {
1104
+ const source = `
1105
+ @on_trigger("claim_reward")
1106
+ fn handle_claim() {
1107
+ say("Claimed!");
1108
+ }
1109
+ `;
1110
+ const files = compile(source);
1111
+ const load = files.find(f => f.path.includes('__load.mcfunction'));
1112
+ expect(load?.content).toContain('scoreboard objectives add claim_reward trigger');
1113
+ expect(load?.content).toContain('scoreboard players enable @a claim_reward');
1114
+ });
1115
+ it('generates trigger check in __tick function', () => {
1116
+ const source = `
1117
+ @on_trigger("claim_reward")
1118
+ fn handle_claim() {
1119
+ say("Claimed!");
1120
+ }
1121
+ `;
1122
+ const files = compile(source);
1123
+ // Trigger checks are now in __tick.mcfunction
1124
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
1125
+ expect(tickFn).toBeDefined();
1126
+ expect(tickFn?.content).toContain('execute as @a[scores={claim_reward=1..}]');
1127
+ expect(tickFn?.content).toContain('run function test:__trigger_claim_reward_dispatch');
1128
+ });
1129
+ it('generates trigger dispatch function', () => {
1130
+ const source = `
1131
+ @on_trigger("claim_reward")
1132
+ fn handle_claim() {
1133
+ say("Claimed!");
1134
+ }
1135
+ `;
1136
+ const files = compile(source);
1137
+ const dispatch = files.find(f => f.path.includes('__trigger_claim_reward_dispatch.mcfunction'));
1138
+ expect(dispatch).toBeDefined();
1139
+ expect(dispatch?.content).toContain('function test:handle_claim');
1140
+ expect(dispatch?.content).toContain('scoreboard players set @s claim_reward 0');
1141
+ expect(dispatch?.content).toContain('scoreboard players enable @s claim_reward');
1142
+ });
1143
+ it('registers __tick in tick tag when triggers exist', () => {
1144
+ const source = `
1145
+ @on_trigger("claim_reward")
1146
+ fn handle_claim() {
1147
+ say("Claimed!");
1148
+ }
1149
+ `;
1150
+ const files = compile(source);
1151
+ const tickTag = files.find(f => f.path === 'data/minecraft/tags/function/tick.json');
1152
+ expect(tickTag).toBeDefined();
1153
+ const content = JSON.parse(tickTag.content);
1154
+ // All tick functionality is routed through __tick
1155
+ expect(content.values).toContain('test:__tick');
1156
+ });
1157
+ it('combines tick functions and trigger check in __tick', () => {
1158
+ const source = `
1159
+ @tick
1160
+ fn game_loop() {
1161
+ say("tick");
1162
+ }
1163
+
1164
+ @on_trigger("claim_reward")
1165
+ fn handle_claim() {
1166
+ say("Claimed!");
1167
+ }
1168
+ `;
1169
+ const files = compile(source);
1170
+ // tick.json points to __tick
1171
+ const tickTag = files.find(f => f.path === 'data/minecraft/tags/function/tick.json');
1172
+ expect(tickTag).toBeDefined();
1173
+ const content = JSON.parse(tickTag.content);
1174
+ expect(content.values).toContain('test:__tick');
1175
+ // __tick.mcfunction calls both tick functions and trigger checks
1176
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
1177
+ expect(tickFn).toBeDefined();
1178
+ expect(tickFn?.content).toContain('function test:game_loop');
1179
+ expect(tickFn?.content).toContain('execute as @a[scores={claim_reward=1..}]');
1180
+ });
1181
+ });
1182
+ describe('Entity tag methods', () => {
1183
+ it('compiles entity.tag()', () => {
1184
+ const source = 'fn test() { @s.tag("boss"); }';
1185
+ const files = compile(source);
1186
+ const fn = getFunction(files, 'test');
1187
+ expect(fn).toContain('tag @s add boss');
1188
+ });
1189
+ it('compiles entity.untag()', () => {
1190
+ const source = 'fn test() { @s.untag("boss"); }';
1191
+ const files = compile(source);
1192
+ const fn = getFunction(files, 'test');
1193
+ expect(fn).toContain('tag @s remove boss');
1194
+ });
1195
+ it('compiles entity.has_tag()', () => {
1196
+ const source = 'fn test() { let x: bool = @s.has_tag("boss"); }';
1197
+ const files = compile(source);
1198
+ const fn = getFunction(files, 'test');
1199
+ expect(fn).toContain('if entity @s[tag=boss]');
1200
+ });
1201
+ });
1202
+ describe('Real program: zombie_game.mcrs', () => {
1203
+ const source = `
1204
+ // A zombie survival game logic
1205
+ // Kills nearby zombies and tracks score
1206
+
1207
+ @tick(rate=20)
1208
+ fn check_zombies() {
1209
+ foreach (z in @e[type=zombie, distance=..10]) {
1210
+ kill(z);
1211
+ }
1212
+ }
1213
+
1214
+ @tick(rate=100)
1215
+ fn announce() {
1216
+ say("Zombie check complete");
1217
+ }
1218
+
1219
+ fn reward_player() {
1220
+ give(@s, "minecraft:diamond", 1);
1221
+ title(@s, "Zombie Slayer!");
1222
+ }
1223
+
1224
+ @on_trigger("claim_reward")
1225
+ fn handle_claim() {
1226
+ reward_player();
1227
+ }
1228
+ `;
1229
+ it('compiles without errors', () => {
1230
+ const files = compile(source, 'zombie');
1231
+ expect(files.length).toBeGreaterThan(0);
1232
+ });
1233
+ it('generates check_zombies with foreach loop', () => {
1234
+ const files = compile(source, 'zombie');
1235
+ // With tick rate, the foreach is in tick_body block
1236
+ const allContent = files
1237
+ .filter(f => f.path.includes('check_zombies'))
1238
+ .map(f => f.content)
1239
+ .join('\n');
1240
+ expect(allContent).toContain('execute as @e[type=minecraft:zombie,distance=..10]');
1241
+ });
1242
+ it('generates foreach sub-function with kill @s', () => {
1243
+ const files = compile(source, 'zombie');
1244
+ const subFn = files.find(f => f.path.includes('check_zombies/foreach_0'));
1245
+ expect(subFn).toBeDefined();
1246
+ expect(subFn?.content).toContain('kill @s');
1247
+ });
1248
+ it('generates announce function with say command', () => {
1249
+ const files = compile(source, 'zombie');
1250
+ const allContent = files
1251
+ .filter(f => f.path.includes('announce'))
1252
+ .map(f => f.content)
1253
+ .join('\n');
1254
+ expect(allContent).toContain('say Zombie check complete');
1255
+ });
1256
+ it('generates reward_player with give and title', () => {
1257
+ const files = compile(source, 'zombie');
1258
+ const fn = getFunction(files, 'reward_player');
1259
+ expect(fn).toContain('give @s minecraft:diamond 1');
1260
+ expect(fn).toContain('title @s title');
1261
+ expect(fn).toContain('Zombie Slayer!');
1262
+ });
1263
+ it('registers __tick in tick tag and calls tick functions', () => {
1264
+ const files = compile(source, 'zombie');
1265
+ const tickTag = files.find(f => f.path === 'data/minecraft/tags/function/tick.json');
1266
+ expect(tickTag).toBeDefined();
1267
+ const content = JSON.parse(tickTag.content);
1268
+ expect(content.values).toContain('zombie:__tick');
1269
+ // __tick should call both tick functions
1270
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
1271
+ expect(tickFn).toBeDefined();
1272
+ expect(tickFn?.content).toContain('function zombie:check_zombies');
1273
+ expect(tickFn?.content).toContain('function zombie:announce');
1274
+ });
1275
+ it('generates trigger infrastructure for claim_reward', () => {
1276
+ const files = compile(source, 'zombie');
1277
+ // Check __load.mcfunction has trigger objective
1278
+ const load = files.find(f => f.path.includes('__load.mcfunction'));
1279
+ expect(load?.content).toContain('scoreboard objectives add claim_reward trigger');
1280
+ // Check dispatch function exists
1281
+ const dispatch = files.find(f => f.path.includes('__trigger_claim_reward_dispatch'));
1282
+ expect(dispatch).toBeDefined();
1283
+ expect(dispatch?.content).toContain('function zombie:handle_claim');
1284
+ // Check trigger_check is in __tick.mcfunction
1285
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
1286
+ expect(tickFn).toBeDefined();
1287
+ expect(tickFn?.content).toContain('execute as @a[scores={claim_reward=1..}]');
1288
+ });
1289
+ it('generates function call from handle_claim to reward_player', () => {
1290
+ const files = compile(source, 'zombie');
1291
+ const fn = getFunction(files, 'handle_claim');
1292
+ expect(fn).toContain('function zombie:reward_player');
1293
+ });
1294
+ });
1295
+ describe('Test 11: Struct types backed by NBT storage', () => {
1296
+ const source = `
1297
+ struct Point { x: int, y: int }
1298
+
1299
+ fn test_struct() {
1300
+ let p: Point = { x: 10, y: 20 };
1301
+ p.x = 30;
1302
+ let val = p.x;
1303
+ }
1304
+ `;
1305
+ it('generates struct field initialization with NBT storage', () => {
1306
+ const files = compile(source, 'structs');
1307
+ const fn = getFunction(files, 'test_struct');
1308
+ expect(fn).toBeDefined();
1309
+ expect(fn).toContain('data modify storage rs:heap point_p.x set value 10');
1310
+ expect(fn).toContain('data modify storage rs:heap point_p.y set value 20');
1311
+ });
1312
+ it('generates struct field assignment', () => {
1313
+ const files = compile(source, 'structs');
1314
+ const fn = getFunction(files, 'test_struct');
1315
+ expect(fn).toContain('data modify storage rs:heap point_p.x set value 30');
1316
+ });
1317
+ it('generates struct field read into scoreboard', () => {
1318
+ const files = compile(source, 'structs');
1319
+ const fn = getFunction(files, 'test_struct');
1320
+ expect(fn).toContain('execute store result score');
1321
+ expect(fn).toContain('data get storage rs:heap point_p.x');
1322
+ });
1323
+ });
1324
+ describe('Test 12: Struct compound assignment', () => {
1325
+ const source = `
1326
+ struct Counter { value: int }
1327
+
1328
+ fn test_compound() {
1329
+ let c: Counter = { value: 0 };
1330
+ c.value += 10;
1331
+ c.value -= 5;
1332
+ }
1333
+ `;
1334
+ it('generates read-modify-write for compound assignment', () => {
1335
+ const files = compile(source, 'compound');
1336
+ const fn = getFunction(files, 'test_compound');
1337
+ expect(fn).toBeDefined();
1338
+ // Should read, add, write back
1339
+ expect(fn).toContain('data get storage rs:heap counter_c.value');
1340
+ expect(fn).toContain('+=');
1341
+ });
1342
+ });
1343
+ describe('Test 13: int[] array type', () => {
1344
+ const source = `
1345
+ fn test_array() {
1346
+ let arr: int[] = [];
1347
+ arr.push(42);
1348
+ arr.push(100);
1349
+ let first = arr[0];
1350
+ }
1351
+ `;
1352
+ it('initializes empty array in NBT storage', () => {
1353
+ const files = compile(source, 'arrays');
1354
+ const fn = getFunction(files, 'test_array');
1355
+ expect(fn).toBeDefined();
1356
+ expect(fn).toContain('data modify storage rs:heap arr set value []');
1357
+ });
1358
+ it('generates array push', () => {
1359
+ const files = compile(source, 'arrays');
1360
+ const fn = getFunction(files, 'test_array');
1361
+ expect(fn).toContain('data modify storage rs:heap arr append value 42');
1362
+ expect(fn).toContain('data modify storage rs:heap arr append value 100');
1363
+ });
1364
+ it('generates array index access', () => {
1365
+ const files = compile(source, 'arrays');
1366
+ const fn = getFunction(files, 'test_array');
1367
+ expect(fn).toContain('data get storage rs:heap arr[0]');
1368
+ });
1369
+ });
1370
+ describe('Test 14: Array with initial values', () => {
1371
+ const source = `
1372
+ fn test_init_array() {
1373
+ let nums: int[] = [1, 2, 3];
1374
+ }
1375
+ `;
1376
+ it('initializes array with values', () => {
1377
+ const files = compile(source, 'initarr');
1378
+ const fn = getFunction(files, 'test_init_array');
1379
+ expect(fn).toBeDefined();
1380
+ expect(fn).toContain('data modify storage rs:heap nums set value []');
1381
+ expect(fn).toContain('data modify storage rs:heap nums append value 1');
1382
+ expect(fn).toContain('data modify storage rs:heap nums append value 2');
1383
+ expect(fn).toContain('data modify storage rs:heap nums append value 3');
1384
+ });
1385
+ });
1386
+ describe('Test 15: World objects (armor stands)', () => {
1387
+ const source = `
1388
+ fn test_spawn() {
1389
+ let turret = spawn_object(10, 64, 20);
1390
+ turret.health = 100;
1391
+ }
1392
+ `;
1393
+ it('generates summon command for world object', () => {
1394
+ const files = compile(source, 'world');
1395
+ const fn = getFunction(files, 'test_spawn');
1396
+ expect(fn).toBeDefined();
1397
+ expect(fn).toContain('summon minecraft:armor_stand 10 64 20');
1398
+ expect(fn).toContain('Invisible:1b');
1399
+ expect(fn).toContain('Marker:1b');
1400
+ expect(fn).toContain('NoGravity:1b');
1401
+ expect(fn).toContain('Tags:["__rs_obj_');
1402
+ });
1403
+ it('generates scoreboard set for world object field', () => {
1404
+ const files = compile(source, 'world');
1405
+ const fn = getFunction(files, 'test_spawn');
1406
+ expect(fn).toContain('scoreboard players set @e[tag=__rs_obj_');
1407
+ expect(fn).toContain('rs 100');
1408
+ });
1409
+ });
1410
+ describe('Test 16: World object compound operations', () => {
1411
+ const source = `
1412
+ fn test_damage() {
1413
+ let obj = spawn_object(0, 64, 0);
1414
+ obj.health = 100;
1415
+ obj.health -= 10;
1416
+ }
1417
+ `;
1418
+ it('generates compound assignment on world object', () => {
1419
+ const files = compile(source, 'damage');
1420
+ const fn = getFunction(files, 'test_damage');
1421
+ expect(fn).toBeDefined();
1422
+ // Should have -= operation
1423
+ expect(fn).toContain('scoreboard players operation @e[tag=__rs_obj_');
1424
+ expect(fn).toContain('-=');
1425
+ });
1426
+ });
1427
+ describe('Test 17: Kill world object', () => {
1428
+ const source = `
1429
+ fn test_kill() {
1430
+ let obj = spawn_object(0, 64, 0);
1431
+ kill(obj);
1432
+ }
1433
+ `;
1434
+ it('generates kill command for world object', () => {
1435
+ const files = compile(source, 'killobj');
1436
+ const fn = getFunction(files, 'test_kill');
1437
+ expect(fn).toBeDefined();
1438
+ expect(fn).toContain('kill @e[tag=__rs_obj_');
1439
+ });
1440
+ });
1441
+ });
1442
+ describe('#mc_name syntax', () => {
1443
+ describe('scoreboard with #mc_name objective', () => {
1444
+ const source = `
1445
+ fn heal(amount: int) {
1446
+ let health: int = scoreboard_get(@p, #health);
1447
+ let next: int = health + amount;
1448
+ scoreboard_set(@p, #health, next);
1449
+ }
1450
+ `;
1451
+ it('compiles #health to bare objective name (no quotes)', () => {
1452
+ const files = compile(source, 'mcname');
1453
+ const fn = getFunction(files, 'heal');
1454
+ expect(fn).toBeDefined();
1455
+ expect(fn).toContain('scoreboard players get @p health');
1456
+ });
1457
+ it('does not produce quoted "health" in output', () => {
1458
+ const files = compile(source, 'mcname');
1459
+ const fn = getFunction(files, 'heal');
1460
+ expect(fn).not.toContain('"health"');
1461
+ });
1462
+ });
1463
+ describe('backward compat: string objective still works', () => {
1464
+ const source = `fn test() { let x: int = scoreboard_get(@s, "kills"); }`;
1465
+ it('prefixes plain string objectives with the active namespace', () => {
1466
+ const files = compile(source, 'compat');
1467
+ const fn = getFunction(files, 'test');
1468
+ expect(fn).toContain('scoreboard players get @s compat.kills');
1469
+ });
1470
+ });
1471
+ describe('#mc_name with fake player target', () => {
1472
+ const source = `
1473
+ fn tick_game() {
1474
+ let timer: int = scoreboard_get(#game, #timer);
1475
+ scoreboard_set(#game, #timer, timer - 1);
1476
+ }
1477
+ `;
1478
+ it('compiles #game fake player and #timer objective correctly', () => {
1479
+ const files = compile(source, 'fakeplay');
1480
+ const fn = getFunction(files, 'tick_game');
1481
+ expect(fn).toBeDefined();
1482
+ expect(fn).toContain('scoreboard players get game timer');
1483
+ expect(fn).toContain('game timer');
1484
+ });
1485
+ });
1486
+ describe('tag_add with #mc_name', () => {
1487
+ const source = `fn mark() { tag_add(@s, #hasKey); }`;
1488
+ it('compiles tag_add with #mc_name', () => {
1489
+ const files = compile(source, 'tagname');
1490
+ const fn = getFunction(files, 'mark');
1491
+ expect(fn).toBeDefined();
1492
+ expect(fn).toContain('tag @s add hasKey');
1493
+ });
1494
+ });
1495
+ describe('gamerule with #mc_name', () => {
1496
+ const source = `fn setup() { gamerule(#keepInventory, true); }`;
1497
+ it('compiles gamerule with #mc_name', () => {
1498
+ const files = compile(source, 'gamerule');
1499
+ const fn = getFunction(files, 'setup');
1500
+ expect(fn).toBeDefined();
1501
+ expect(fn).toContain('gamerule keepInventory');
1502
+ });
1503
+ });
1504
+ describe('NBT literals', () => {
1505
+ it('compiles byte literal', () => {
1506
+ const files = compile('fn test() -> int { let x: byte = 20b; return x; }', 'nbt');
1507
+ const fn = getFunction(files, 'test');
1508
+ expect(fn).toBeDefined();
1509
+ expect(fn).toContain('20');
1510
+ });
1511
+ it('compiles short literal', () => {
1512
+ const files = compile('fn test() -> int { let x: short = 100s; return x; }', 'nbt');
1513
+ const fn = getFunction(files, 'test');
1514
+ expect(fn).toBeDefined();
1515
+ expect(fn).toContain('100');
1516
+ });
1517
+ it('compiles long literal', () => {
1518
+ const files = compile('fn test() -> int { let x: long = 1000L; return x; }', 'nbt');
1519
+ const fn = getFunction(files, 'test');
1520
+ expect(fn).toBeDefined();
1521
+ expect(fn).toContain('1000');
1522
+ });
1523
+ it('compiles double literal', () => {
1524
+ const files = compile('fn test() -> int { let x: double = 3.14d; return x; }', 'nbt');
1525
+ const fn = getFunction(files, 'test');
1526
+ expect(fn).toBeDefined();
1527
+ });
1528
+ it('compiles float literal with f suffix', () => {
1529
+ const files = compile('fn test() -> int { let x: float = 2.5f; return x; }', 'nbt');
1530
+ const fn = getFunction(files, 'test');
1531
+ expect(fn).toBeDefined();
1532
+ });
1533
+ it('type-checks NBT literals without errors', () => {
1534
+ const errors = typeCheck(`
1535
+ fn test() {
1536
+ let a = 20b;
1537
+ let b = 100s;
1538
+ let c = 1000L;
1539
+ let d = 3.14d;
1540
+ let e = 2.5f;
1541
+ }
1542
+ `);
1543
+ expect(errors).toHaveLength(0);
1544
+ });
1545
+ });
1546
+ // ---------------------------------------------------------------------------
1547
+ // Type Inference
1548
+ // ---------------------------------------------------------------------------
1549
+ describe('Type inference', () => {
1550
+ it('infers int type', () => {
1551
+ const errors = typeCheck(`
1552
+ fn test() {
1553
+ let x = 5;
1554
+ let y: int = x + 1;
1555
+ }
1556
+ `);
1557
+ expect(errors).toHaveLength(0);
1558
+ });
1559
+ it('infers string type', () => {
1560
+ const errors = typeCheck(`
1561
+ fn test() {
1562
+ let x = "hello";
1563
+ let y: string = x;
1564
+ }
1565
+ `);
1566
+ expect(errors).toHaveLength(0);
1567
+ });
1568
+ it('infers bool type', () => {
1569
+ const errors = typeCheck(`
1570
+ fn test() {
1571
+ let x = true;
1572
+ let y: bool = x;
1573
+ }
1574
+ `);
1575
+ expect(errors).toHaveLength(0);
1576
+ });
1577
+ it('infers from function return', () => {
1578
+ const errors = typeCheck(`
1579
+ fn get_value() -> int { return 42; }
1580
+ fn test() {
1581
+ let x = get_value();
1582
+ let y: int = x + 1;
1583
+ }
1584
+ `);
1585
+ expect(errors).toHaveLength(0);
1586
+ });
1587
+ it('infers NBT types from suffix', () => {
1588
+ const files = compile(`
1589
+ fn test() {
1590
+ let a = 20b;
1591
+ let b = 100s;
1592
+ let c = 1000L;
1593
+ let d = 3.14d;
1594
+ }
1595
+ `);
1596
+ expect(files.length).toBeGreaterThan(0);
1597
+ });
1598
+ it('detects type mismatch with inferred type', () => {
1599
+ const errors = typeCheck(`
1600
+ fn test() {
1601
+ let x = 5;
1602
+ let y: string = x;
1603
+ }
1604
+ `);
1605
+ expect(errors.length).toBeGreaterThan(0);
1606
+ });
1607
+ it('compiles let without type annotation', () => {
1608
+ const files = compile(`
1609
+ fn test() {
1610
+ let x = 5;
1611
+ let y = x + 1;
1612
+ }
1613
+ `);
1614
+ expect(files.length).toBeGreaterThan(0);
1615
+ });
1616
+ });
1617
+ });
1618
+ describe('for-range loop', () => {
1619
+ it('compiles basic for-range loop', () => {
1620
+ const src = `fn test() { for i in 0..5 { say("hi"); } }`;
1621
+ const files = compile(src, 'forloop');
1622
+ expect(files.some(f => f.path.includes('__for'))).toBe(true);
1623
+ });
1624
+ it('initializes loop variable', () => {
1625
+ const src = `fn test() { for i in 0..5 { say("hi"); } }`;
1626
+ const files = compile(src, 'forloop');
1627
+ const fn = getFunction(files, 'test');
1628
+ expect(fn).toContain('scoreboard players set $test_i rs 0');
1629
+ });
1630
+ it('generates loop sub-function with increment and condition', () => {
1631
+ const src = `fn test() { for i in 0..5 { say("hi"); } }`;
1632
+ const files = compile(src, 'forloop');
1633
+ const subFn = files.find(f => f.path.includes('__for_0'));
1634
+ expect(subFn).toBeDefined();
1635
+ expect(subFn?.content).toContain('say hi');
1636
+ expect(subFn?.content).toContain('scoreboard players add $test_i rs 1');
1637
+ expect(subFn?.content).toContain('execute if score $test_i rs matches ..4 run function forloop:test/__for_0');
1638
+ });
1639
+ it('supports non-zero start', () => {
1640
+ const src = `fn test() { for x in 3..8 { say("loop"); } }`;
1641
+ const files = compile(src, 'forloop2');
1642
+ const fn = getFunction(files, 'test');
1643
+ expect(fn).toContain('scoreboard players set $test_x rs 3');
1644
+ const subFn = files.find(f => f.path.includes('__for_0'));
1645
+ expect(subFn?.content).toContain('execute if score $test_x rs matches ..7 run function forloop2:test/__for_0');
1646
+ });
1647
+ });
1648
+ // ---------------------------------------------------------------------------
1649
+ // NBT Structured Parameters
1650
+ // ---------------------------------------------------------------------------
1651
+ describe('NBT parameters', () => {
1652
+ it('compiles give with NBT struct', () => {
1653
+ const src = `fn test() { give(@s, "minecraft:diamond_sword", 1, { display: { Name: "Excalibur" } }); }`;
1654
+ const files = compile(src, 'nbtparam');
1655
+ const fn = getFunction(files, 'test');
1656
+ expect(fn).toContain('give @s minecraft:diamond_sword{display:{Name:"Excalibur"}} 1');
1657
+ });
1658
+ it('compiles give with nested NBT and arrays', () => {
1659
+ const src = `fn test() { give(@s, "minecraft:stick", 1, { display: { Name: "Magic Wand" }, Enchantments: [{ id: "sharpness", lvl: 5 }] }); }`;
1660
+ const files = compile(src, 'nbtparam2');
1661
+ const fn = getFunction(files, 'test');
1662
+ expect(fn).toContain('{display:{Name:"Magic Wand"},Enchantments:[{id:"sharpness",lvl:5}]}');
1663
+ });
1664
+ it('compiles summon with NBT', () => {
1665
+ const src = `fn test() { summon("minecraft:zombie", 0, 64, 0, { CustomName: "Boss", NoAI: true }); }`;
1666
+ const files = compile(src, 'nbtsummon');
1667
+ const fn = getFunction(files, 'test');
1668
+ expect(fn).toContain('summon minecraft:zombie 0 64 0 {CustomName:"Boss",NoAI:1b}');
1669
+ });
1670
+ it('compiles give with bool values in NBT', () => {
1671
+ const src = `fn test() { give(@s, "minecraft:shield", 1, { Unbreakable: true }); }`;
1672
+ const files = compile(src, 'nbtbool');
1673
+ const fn = getFunction(files, 'test');
1674
+ expect(fn).toContain('{Unbreakable:1b}');
1675
+ });
1676
+ });
1677
+ // ---------------------------------------------------------------------------
1678
+ // Set Operations
1679
+ // ---------------------------------------------------------------------------
1680
+ describe('Set operations', () => {
1681
+ it('creates a new set', () => {
1682
+ const src = `fn test() { let s = set_new(); }`;
1683
+ const files = compile(src, 'settest');
1684
+ const fn = getFunction(files, 'test');
1685
+ expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
1686
+ });
1687
+ it('adds to a set with uniqueness check', () => {
1688
+ const src = `fn test() { let s = set_new(); set_add(s, "apple"); }`;
1689
+ const files = compile(src, 'setadd');
1690
+ const fn = getFunction(files, 'test');
1691
+ expect(fn).toContain('execute unless data storage rs:sets __set_0[{value:apple}] run data modify storage rs:sets __set_0 append value {value:apple}');
1692
+ });
1693
+ it('checks set membership', () => {
1694
+ const src = `fn test() { let s = set_new(); set_add(s, "x"); let has = set_contains(s, "x"); }`;
1695
+ const files = compile(src, 'setcontains');
1696
+ const fn = getFunction(files, 'test');
1697
+ expect(fn).toContain('if data storage rs:sets __set_0[{value:x}]');
1698
+ });
1699
+ it('removes from a set', () => {
1700
+ const src = `fn test() { let s = set_new(); set_add(s, "y"); set_remove(s, "y"); }`;
1701
+ const files = compile(src, 'setremove');
1702
+ const fn = getFunction(files, 'test');
1703
+ expect(fn).toContain('data remove storage rs:sets __set_0[{value:y}]');
1704
+ });
1705
+ it('clears a set', () => {
1706
+ const src = `fn test() { let s = set_new(); set_clear(s); }`;
1707
+ const files = compile(src, 'setclear');
1708
+ const fn = getFunction(files, 'test');
1709
+ expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
1710
+ });
1711
+ });
1712
+ // ---------------------------------------------------------------------------
1713
+ // Method Syntax Sugar
1714
+ // ---------------------------------------------------------------------------
1715
+ describe('Method syntax sugar', () => {
1716
+ it('transforms obj.method() to method(obj)', () => {
1717
+ const src = `fn test() { let s = set_new(); s.clear(); }`;
1718
+ const files = compile(src, 'method1');
1719
+ const fn = getFunction(files, 'test');
1720
+ expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
1721
+ });
1722
+ it('transforms obj.method(arg) to method(obj, arg)', () => {
1723
+ const src = `fn test() { let s = set_new(); s.add("apple"); }`;
1724
+ const files = compile(src, 'method2');
1725
+ const fn = getFunction(files, 'test');
1726
+ expect(fn).toContain('data modify storage rs:sets __set_0 append value {value:apple}');
1727
+ });
1728
+ it('transforms obj.method(arg) with contains', () => {
1729
+ const src = `fn test() { let s = set_new(); s.add("x"); let r = s.contains("x"); }`;
1730
+ const files = compile(src, 'method3');
1731
+ const fn = getFunction(files, 'test');
1732
+ expect(fn).toBeDefined();
1733
+ });
1734
+ it('works with multiple args', () => {
1735
+ const src = `fn test() { let s = set_new(); s.add("a"); s.add("b"); s.remove("a"); }`;
1736
+ const files = compile(src, 'method4');
1737
+ const fn = getFunction(files, 'test');
1738
+ expect(fn).toContain('data remove storage rs:sets __set_0[{value:a}]');
1739
+ });
1740
+ });
1741
+ describe('Global variables', () => {
1742
+ it('initializes global in __load', () => {
1743
+ const src = `let x: int = 42;\nfn test() { say("hi"); }`;
1744
+ const files = compile(src, 'globaltest');
1745
+ const load = getFunction(files, '__load');
1746
+ expect(load).toContain('scoreboard players set $x rs 42');
1747
+ });
1748
+ it('reads and writes global in function', () => {
1749
+ const src = `let count: int = 0;\nfn inc() { count = count + 1; }`;
1750
+ const files = compile(src, 'globalrw');
1751
+ const fn = getFunction(files, 'inc');
1752
+ expect(fn).toBeDefined();
1753
+ // Global should be initialized in __load
1754
+ const load = getFunction(files, '__load');
1755
+ expect(load).toContain('scoreboard players set $count rs 0');
1756
+ });
1757
+ it('const cannot be reassigned', () => {
1758
+ const src = `const X: int = 5;\nfn bad() { X = 10; }`;
1759
+ expect(() => compile(src, 'constbad')).toThrow();
1760
+ });
1761
+ });
1762
+ describe('@load decorator', () => {
1763
+ it('calls @load function from __load.mcfunction', () => {
1764
+ const src = `@load fn init() { say("Datapack loaded!"); }`;
1765
+ const files = compile(src, 'loadtest');
1766
+ const load = getFunction(files, '__load');
1767
+ expect(load).toContain('function loadtest:init');
1768
+ });
1769
+ it('calls multiple @load functions in order', () => {
1770
+ const src = `
1771
+ @load fn setup() { say("setup"); }
1772
+ @load fn init() { say("init"); }
1773
+ `;
1774
+ const files = compile(src, 'loadtest');
1775
+ const load = getFunction(files, '__load');
1776
+ const setupIdx = load.indexOf('function loadtest:setup');
1777
+ const initIdx = load.indexOf('function loadtest:init');
1778
+ expect(setupIdx).toBeGreaterThan(-1);
1779
+ expect(initIdx).toBeGreaterThan(-1);
1780
+ expect(setupIdx).toBeLessThan(initIdx);
1781
+ });
1782
+ it('generates the @load function body normally', () => {
1783
+ const src = `@load fn init() { say("hi"); }`;
1784
+ const files = compile(src, 'loadtest');
1785
+ const fn = getFunction(files, 'init');
1786
+ expect(fn).toContain('say hi');
1787
+ });
1788
+ });
1789
+ //# sourceMappingURL=e2e.test.js.map