redscript-mc 1.0.0 → 1.2.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 (136) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.yml +72 -0
  2. package/.github/ISSUE_TEMPLATE/feature_request.yml +57 -0
  3. package/.github/PULL_REQUEST_TEMPLATE.md +17 -25
  4. package/CHANGELOG.md +112 -0
  5. package/CONTRIBUTING.md +140 -0
  6. package/README.md +28 -19
  7. package/README.zh.md +28 -19
  8. package/dist/__tests__/cli.test.js +148 -10
  9. package/dist/__tests__/codegen.test.js +26 -1
  10. package/dist/__tests__/diagnostics.test.js +5 -5
  11. package/dist/__tests__/e2e.test.js +336 -17
  12. package/dist/__tests__/formatter.test.d.ts +1 -0
  13. package/dist/__tests__/formatter.test.js +40 -0
  14. package/dist/__tests__/lexer.test.js +12 -2
  15. package/dist/__tests__/lowering.test.js +200 -12
  16. package/dist/__tests__/mc-integration.test.js +370 -31
  17. package/dist/__tests__/mc-syntax.test.js +3 -3
  18. package/dist/__tests__/nbt.test.js +2 -2
  19. package/dist/__tests__/optimizer-advanced.test.js +5 -5
  20. package/dist/__tests__/parser.test.js +80 -0
  21. package/dist/__tests__/runtime.test.js +9 -9
  22. package/dist/__tests__/typechecker.test.js +158 -0
  23. package/dist/ast/types.d.ts +40 -3
  24. package/dist/cli.js +25 -7
  25. package/dist/codegen/mcfunction/index.d.ts +1 -1
  26. package/dist/codegen/mcfunction/index.js +38 -3
  27. package/dist/codegen/structure/index.js +32 -1
  28. package/dist/compile.d.ts +10 -0
  29. package/dist/compile.js +36 -5
  30. package/dist/events/types.d.ts +35 -0
  31. package/dist/events/types.js +59 -0
  32. package/dist/formatter/index.d.ts +1 -0
  33. package/dist/formatter/index.js +26 -0
  34. package/dist/index.js +3 -2
  35. package/dist/ir/builder.d.ts +2 -1
  36. package/dist/ir/types.d.ts +11 -2
  37. package/dist/ir/types.js +1 -1
  38. package/dist/lexer/index.d.ts +1 -1
  39. package/dist/lexer/index.js +2 -0
  40. package/dist/lowering/index.d.ts +34 -1
  41. package/dist/lowering/index.js +622 -23
  42. package/dist/mc-test/runner.d.ts +2 -2
  43. package/dist/mc-test/runner.js +3 -3
  44. package/dist/mc-test/setup.js +2 -2
  45. package/dist/parser/index.d.ts +4 -0
  46. package/dist/parser/index.js +153 -16
  47. package/dist/typechecker/index.d.ts +17 -0
  48. package/dist/typechecker/index.js +343 -17
  49. package/docs/COMPILATION_STATS.md +24 -24
  50. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  51. package/docs/IMPLEMENTATION_GUIDE.md +1 -1
  52. package/docs/STRUCTURE_TARGET.md +1 -1
  53. package/editors/vscode/.vscodeignore +1 -0
  54. package/editors/vscode/CHANGELOG.md +9 -0
  55. package/editors/vscode/icons/mcrs.svg +7 -0
  56. package/editors/vscode/icons/redscript-icons.json +10 -0
  57. package/editors/vscode/out/extension.js +1295 -80
  58. package/editors/vscode/package-lock.json +2 -2
  59. package/editors/vscode/package.json +10 -3
  60. package/editors/vscode/src/hover.ts +55 -2
  61. package/editors/vscode/src/symbols.ts +42 -0
  62. package/package.json +1 -1
  63. package/src/__tests__/cli.test.ts +176 -10
  64. package/src/__tests__/codegen.test.ts +28 -1
  65. package/src/__tests__/diagnostics.test.ts +5 -5
  66. package/src/__tests__/e2e.test.ts +335 -17
  67. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  68. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  69. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  70. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  71. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  72. package/src/__tests__/lexer.test.ts +14 -2
  73. package/src/__tests__/lowering.test.ts +226 -12
  74. package/src/__tests__/mc-integration.test.ts +421 -31
  75. package/src/__tests__/mc-syntax.test.ts +3 -3
  76. package/src/__tests__/nbt.test.ts +2 -2
  77. package/src/__tests__/optimizer-advanced.test.ts +5 -5
  78. package/src/__tests__/parser.test.ts +91 -5
  79. package/src/__tests__/runtime.test.ts +9 -9
  80. package/src/__tests__/typechecker.test.ts +171 -0
  81. package/src/ast/types.ts +44 -3
  82. package/src/cli.ts +10 -10
  83. package/src/codegen/mcfunction/index.ts +40 -3
  84. package/src/codegen/structure/index.ts +35 -1
  85. package/src/compile.ts +54 -6
  86. package/src/events/types.ts +69 -0
  87. package/src/examples/capture_the_flag.mcrs +208 -0
  88. package/src/examples/{counter.rs → counter.mcrs} +1 -1
  89. package/src/examples/hunger_games.mcrs +301 -0
  90. package/src/examples/new_features_demo.mcrs +193 -0
  91. package/src/examples/parkour_race.mcrs +233 -0
  92. package/src/examples/rpg.mcrs +13 -0
  93. package/src/examples/{shop.rs → shop.mcrs} +1 -1
  94. package/src/examples/{showcase_game.rs → showcase_game.mcrs} +3 -3
  95. package/src/examples/{turret.rs → turret.mcrs} +1 -1
  96. package/src/examples/zombie_survival.mcrs +314 -0
  97. package/src/index.ts +4 -3
  98. package/src/ir/builder.ts +3 -1
  99. package/src/ir/types.ts +12 -2
  100. package/src/lexer/index.ts +3 -1
  101. package/src/lowering/index.ts +684 -24
  102. package/src/mc-test/runner.ts +3 -3
  103. package/src/mc-test/setup.ts +2 -2
  104. package/src/parser/index.ts +170 -19
  105. package/src/stdlib/README.md +178 -140
  106. package/src/stdlib/bossbar.mcrs +68 -0
  107. package/src/stdlib/{cooldown.rs → cooldown.mcrs} +1 -1
  108. package/src/stdlib/effects.mcrs +64 -0
  109. package/src/stdlib/interactions.mcrs +195 -0
  110. package/src/stdlib/inventory.mcrs +38 -0
  111. package/src/stdlib/mobs.mcrs +99 -0
  112. package/src/stdlib/particles.mcrs +52 -0
  113. package/src/stdlib/sets.mcrs +20 -0
  114. package/src/stdlib/spawn.mcrs +41 -0
  115. package/src/stdlib/tags.mcrs +951 -0
  116. package/src/stdlib/teams.mcrs +68 -0
  117. package/src/stdlib/timer.mcrs +72 -0
  118. package/src/stdlib/world.mcrs +92 -0
  119. package/src/typechecker/index.ts +404 -18
  120. package/src/examples/rpg.rs +0 -13
  121. package/src/stdlib/mobs.rs +0 -99
  122. package/src/stdlib/timer.rs +0 -51
  123. /package/src/examples/{arena.rs → arena.mcrs} +0 -0
  124. /package/src/examples/{pvp_arena.rs → pvp_arena.mcrs} +0 -0
  125. /package/src/examples/{quiz.rs → quiz.mcrs} +0 -0
  126. /package/src/examples/{stdlib_demo.rs → stdlib_demo.mcrs} +0 -0
  127. /package/src/examples/{world_manager.rs → world_manager.mcrs} +0 -0
  128. /package/src/stdlib/{combat.rs → combat.mcrs} +0 -0
  129. /package/src/stdlib/{math.rs → math.mcrs} +0 -0
  130. /package/src/stdlib/{player.rs → player.mcrs} +0 -0
  131. /package/src/stdlib/{strings.rs → strings.mcrs} +0 -0
  132. /package/src/templates/{combat.rs → combat.mcrs} +0 -0
  133. /package/src/templates/{economy.rs → economy.mcrs} +0 -0
  134. /package/src/templates/{mini-game-framework.rs → mini-game-framework.mcrs} +0 -0
  135. /package/src/templates/{quest.rs → quest.mcrs} +0 -0
  136. /package/src/test_programs/{zombie_game.rs → zombie_game.mcrs} +0 -0
@@ -124,6 +124,184 @@ fn main() {
124
124
  expect(mainFn).toContain('"objective":"rs"');
125
125
  });
126
126
  });
127
+ describe('timer builtins', () => {
128
+ it('generates scheduled timer helper functions', () => {
129
+ const files = compile(`
130
+ fn main() {
131
+ let intervalId: int = setInterval(20, () => {
132
+ say("tick");
133
+ });
134
+ setTimeout(100, () => {
135
+ say("later");
136
+ });
137
+ clearInterval(intervalId);
138
+ }
139
+ `);
140
+ const mainFn = getFunction(files, 'main');
141
+ const intervalFn = getFunction(files, '__interval_0');
142
+ const intervalBodyFn = getFunction(files, '__interval_body_0');
143
+ const timeoutFn = getFunction(files, '__timeout_0');
144
+ expect(mainFn).toBeDefined();
145
+ expect(mainFn).toContain('schedule function test:__interval_0 20t');
146
+ expect(mainFn).toContain('schedule function test:__timeout_0 100t');
147
+ expect(mainFn).toContain('schedule clear test:__interval_0');
148
+ expect(intervalFn).toContain('function test:__interval_body_0');
149
+ expect(intervalFn).toContain('schedule function test:__interval_0 20t');
150
+ expect(intervalBodyFn).toContain('say tick');
151
+ expect(timeoutFn).toContain('say later');
152
+ });
153
+ });
154
+ describe('is type narrowing', () => {
155
+ it('type checks and compiles entity narrowing inside foreach blocks', () => {
156
+ const source = `
157
+ fn main() {
158
+ foreach (e in @e) {
159
+ if (e is Player) {
160
+ kill(e);
161
+ }
162
+ if (e is Zombie) {
163
+ kill(e);
164
+ }
165
+ }
166
+ }
167
+ `;
168
+ expect(typeCheck(source)).toEqual([]);
169
+ const files = compile(source);
170
+ const mainFn = getFunction(files, 'main');
171
+ const foreachFn = getSubFunction(files, 'main', 'foreach_0');
172
+ const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'));
173
+ expect(mainFn).toContain('execute as @e run function test:main/foreach_0');
174
+ expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_');
175
+ expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_');
176
+ expect(thenFiles).toHaveLength(2);
177
+ });
178
+ });
179
+ describe('impl blocks', () => {
180
+ it('compiles static and instance impl methods end to end', () => {
181
+ const source = `
182
+ struct Point { x: int, y: int }
183
+
184
+ impl Point {
185
+ fn new(x: int, y: int) -> Point {
186
+ return { x: x, y: y };
187
+ }
188
+
189
+ fn distance(self) -> int {
190
+ return self.x + self.y;
191
+ }
192
+ }
193
+
194
+ fn main() {
195
+ let p: Point = Point::new(1, 2);
196
+ let d: int = p.distance();
197
+ say("\${d}");
198
+ }
199
+ `;
200
+ expect(typeCheck(source)).toEqual([]);
201
+ const files = compile(source);
202
+ const mainFn = getFunction(files, 'main');
203
+ const staticFn = getFunction(files, 'Point_new');
204
+ const instanceFn = getFunction(files, 'Point_distance');
205
+ expect(mainFn).toContain('function test:Point_new');
206
+ expect(mainFn).toContain('function test:Point_distance');
207
+ expect(staticFn).toBeDefined();
208
+ expect(instanceFn).toBeDefined();
209
+ });
210
+ });
211
+ describe('namespace prefixing', () => {
212
+ it('prefixes user objectives but preserves mc_name and qualified objectives', () => {
213
+ const files = compile(`
214
+ fn main() {
215
+ scoreboard_set("timer", #rs, 100);
216
+ scoreboard_set(@s, "timer", 100);
217
+ scoreboard_set(@s, #health, 20);
218
+ scoreboard_set(@s, "custom.timer", 1);
219
+ }
220
+ `, 'pack');
221
+ const mainFn = getFunction(files, 'main');
222
+ expect(mainFn).toContain('scoreboard players set timer rs 100');
223
+ expect(mainFn).toContain('scoreboard players set @s pack.timer 100');
224
+ expect(mainFn).toContain('scoreboard players set @s health 20');
225
+ expect(mainFn).toContain('scoreboard players set @s custom.timer 1');
226
+ });
227
+ });
228
+ describe('Timer OOP API', () => {
229
+ it('compiles the Timer impl API end to end', () => {
230
+ const source = `
231
+ struct Timer {
232
+ _id: int,
233
+ _duration: int
234
+ }
235
+
236
+ impl Timer {
237
+ fn new(duration: int) -> Timer {
238
+ scoreboard_set("timer_ticks", #rs, 0);
239
+ scoreboard_set("timer_active", #rs, 0);
240
+ return { _id: 0, _duration: duration };
241
+ }
242
+
243
+ fn start(self) {
244
+ scoreboard_set("timer_active", #rs, 1);
245
+ }
246
+
247
+ fn pause(self) {
248
+ scoreboard_set("timer_active", #rs, 0);
249
+ }
250
+
251
+ fn reset(self) {
252
+ scoreboard_set("timer_ticks", #rs, 0);
253
+ }
254
+
255
+ fn done(self) -> bool {
256
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
257
+ return ticks >= self._duration;
258
+ }
259
+
260
+ fn tick(self) {
261
+ let active: int = scoreboard_get("timer_active", #rs);
262
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
263
+
264
+ if (active == 1) {
265
+ if (ticks < self._duration) {
266
+ scoreboard_set("timer_ticks", #rs, ticks + 1);
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ fn main() {
273
+ let t: Timer = Timer::new(100);
274
+ t.start();
275
+ t.tick();
276
+ let finished: bool = t.done();
277
+ if (finished) {
278
+ say("done");
279
+ }
280
+ t.pause();
281
+ t.reset();
282
+ }
283
+ `;
284
+ expect(typeCheck(source)).toEqual([]);
285
+ const files = compile(source, 'timerapi');
286
+ const mainFn = getFunction(files, 'main');
287
+ const newFn = getFunction(files, 'Timer_new');
288
+ const startFn = getFunction(files, 'Timer_start');
289
+ const tickFn = getFunction(files, 'Timer_tick');
290
+ const doneFn = getFunction(files, 'Timer_done');
291
+ const pauseFn = getFunction(files, 'Timer_pause');
292
+ const resetFn = getFunction(files, 'Timer_reset');
293
+ expect(mainFn).toContain('function timerapi:Timer_new');
294
+ expect(mainFn).toContain('function timerapi:Timer_start');
295
+ expect(mainFn).toContain('function timerapi:Timer_tick');
296
+ expect(mainFn).toContain('function timerapi:Timer_done');
297
+ expect(newFn).toContain('scoreboard players set timer_ticks rs 0');
298
+ expect(startFn).toContain('scoreboard players set timer_active rs 1');
299
+ expect(tickFn).toContain('scoreboard players get timer_active rs');
300
+ expect(doneFn).toContain('scoreboard players get timer_ticks rs');
301
+ expect(pauseFn).toContain('scoreboard players set timer_active rs 0');
302
+ expect(resetFn).toContain('scoreboard players set timer_ticks rs 0');
303
+ });
304
+ });
127
305
  describe('advancement event decorators', () => {
128
306
  it('generates advancement json with reward function path', () => {
129
307
  const source = `
@@ -276,12 +454,12 @@ fn test() {
276
454
  }
277
455
  `;
278
456
  const fn = getFunction(compile(source), 'test');
279
- expect(fn).toContain('scoreboard objectives setdisplay sidebar kills');
280
- expect(fn).toContain('scoreboard objectives setdisplay list coins');
281
- expect(fn).toContain('scoreboard objectives setdisplay belowName hp');
457
+ expect(fn).toContain('scoreboard objectives setdisplay sidebar test.kills');
458
+ expect(fn).toContain('scoreboard objectives setdisplay list test.coins');
459
+ expect(fn).toContain('scoreboard objectives setdisplay belowName test.hp');
282
460
  expect(fn).toContain('scoreboard objectives setdisplay sidebar');
283
- expect(fn).toContain('scoreboard objectives add kills playerKillCount "Kill Count"');
284
- expect(fn).toContain('scoreboard objectives remove kills');
461
+ expect(fn).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
462
+ expect(fn).toContain('scoreboard objectives remove test.kills');
285
463
  });
286
464
  it('compiles bossbar builtins', () => {
287
465
  const source = `
@@ -306,7 +484,7 @@ fn test() {
306
484
  expect(fn).toContain('bossbar set ns:health visible true');
307
485
  expect(fn).toContain('bossbar set ns:health players @a');
308
486
  expect(fn).toContain('bossbar remove ns:health');
309
- expect(fn).toMatch(/execute store result score \$t\d+ rs run bossbar get ns:health value/);
487
+ expect(fn).toMatch(/execute store result score \$_\d+ rs run bossbar get ns:health value/);
310
488
  });
311
489
  it('compiles team builtins', () => {
312
490
  const source = `
@@ -485,7 +663,7 @@ fn test() -> int {
485
663
  const fn = getFunction(files, 'test');
486
664
  expect(fn).toBeDefined();
487
665
  expect(fn).toContain('execute store result score');
488
- expect(fn).toContain('scoreboard players get PlayerName kill_count');
666
+ expect(fn).toContain('scoreboard players get PlayerName test.kill_count');
489
667
  });
490
668
  it('compiles scoreboard_get with @s selector', () => {
491
669
  const source = `
@@ -497,7 +675,7 @@ fn test() -> int {
497
675
  const files = compile(source);
498
676
  const fn = getFunction(files, 'test');
499
677
  expect(fn).toBeDefined();
500
- expect(fn).toContain('scoreboard players get @s kill_count');
678
+ expect(fn).toContain('scoreboard players get @s test.kill_count');
501
679
  });
502
680
  it('compiles scoreboard_set with constant value', () => {
503
681
  const source = `
@@ -508,7 +686,7 @@ fn test() {
508
686
  const files = compile(source);
509
687
  const fn = getFunction(files, 'test');
510
688
  expect(fn).toBeDefined();
511
- expect(fn).toContain('scoreboard players set PlayerName kill_count 100');
689
+ expect(fn).toContain('scoreboard players set PlayerName test.kill_count 100');
512
690
  });
513
691
  it('compiles scoreboard_set with variable value', () => {
514
692
  const source = `
@@ -522,7 +700,7 @@ fn test() {
522
700
  .filter(f => f.path.includes('test'))
523
701
  .map(f => f.content)
524
702
  .join('\n');
525
- expect(allContent).toContain('execute store result score @s score');
703
+ expect(allContent).toContain('execute store result score @s test.score');
526
704
  });
527
705
  it('compiles score() as expression', () => {
528
706
  const source = `
@@ -548,7 +726,7 @@ fn double_score() -> int {
548
726
  const files = compile(source);
549
727
  const fn = getFunction(files, 'double_score');
550
728
  expect(fn).toBeDefined();
551
- expect(fn).toContain('scoreboard players get @s points');
729
+ expect(fn).toContain('scoreboard players get @s test.points');
552
730
  });
553
731
  });
554
732
  describe('Built-in functions', () => {
@@ -586,19 +764,19 @@ fn double_score() -> int {
586
764
  const source = 'fn test() { let x: int = random(1, 10); }';
587
765
  const files = compile(source);
588
766
  const fn = getFunction(files, 'test');
589
- expect(fn).toContain('scoreboard players random $t0 rs 1 10');
767
+ expect(fn).toContain('scoreboard players random $_0 rs 1 10');
590
768
  });
591
769
  it('compiles random_native()', () => {
592
770
  const source = 'fn test() { let x: int = random_native(1, 6); }';
593
771
  const files = compile(source);
594
772
  const fn = getFunction(files, 'test');
595
- expect(fn).toContain('execute store result score $t0 rs run random value 1 6');
773
+ expect(fn).toContain('execute store result score $_0 rs run random value 1 6');
596
774
  });
597
775
  it('compiles random_native() with zero min', () => {
598
776
  const source = 'fn test() { let x: int = random_native(0, 100); }';
599
777
  const files = compile(source);
600
778
  const fn = getFunction(files, 'test');
601
- expect(fn).toContain('execute store result score $t0 rs run random value 0 100');
779
+ expect(fn).toContain('execute store result score $_0 rs run random value 0 100');
602
780
  });
603
781
  it('compiles random_sequence()', () => {
604
782
  const source = 'fn test() { random_sequence("loot"); random_sequence("loot", 9); }';
@@ -1079,7 +1257,7 @@ fn handle_claim() {
1079
1257
  expect(fn).toContain('if entity @s[tag=boss]');
1080
1258
  });
1081
1259
  });
1082
- describe('Real program: zombie_game.rs', () => {
1260
+ describe('Real program: zombie_game.mcrs', () => {
1083
1261
  const source = `
1084
1262
  // A zombie survival game logic
1085
1263
  // Kills nearby zombies and tracks score
@@ -1342,10 +1520,10 @@ fn heal(amount: int) {
1342
1520
  });
1343
1521
  describe('backward compat: string objective still works', () => {
1344
1522
  const source = `fn test() { let x: int = scoreboard_get(@s, "kills"); }`;
1345
- it('compiles "kills" string to bare objective name', () => {
1523
+ it('prefixes plain string objectives with the active namespace', () => {
1346
1524
  const files = compile(source, 'compat');
1347
1525
  const fn = getFunction(files, 'test');
1348
- expect(fn).toContain('scoreboard players get @s kills');
1526
+ expect(fn).toContain('scoreboard players get @s compat.kills');
1349
1527
  });
1350
1528
  });
1351
1529
  describe('#mc_name with fake player target', () => {
@@ -1525,4 +1703,145 @@ describe('for-range loop', () => {
1525
1703
  expect(subFn?.content).toContain('execute if score $x rs matches ..7 run function forloop2:test/__for_0');
1526
1704
  });
1527
1705
  });
1706
+ // ---------------------------------------------------------------------------
1707
+ // NBT Structured Parameters
1708
+ // ---------------------------------------------------------------------------
1709
+ describe('NBT parameters', () => {
1710
+ it('compiles give with NBT struct', () => {
1711
+ const src = `fn test() { give(@s, "minecraft:diamond_sword", 1, { display: { Name: "Excalibur" } }); }`;
1712
+ const files = compile(src, 'nbtparam');
1713
+ const fn = getFunction(files, 'test');
1714
+ expect(fn).toContain('give @s minecraft:diamond_sword{display:{Name:"Excalibur"}} 1');
1715
+ });
1716
+ it('compiles give with nested NBT and arrays', () => {
1717
+ const src = `fn test() { give(@s, "minecraft:stick", 1, { display: { Name: "Magic Wand" }, Enchantments: [{ id: "sharpness", lvl: 5 }] }); }`;
1718
+ const files = compile(src, 'nbtparam2');
1719
+ const fn = getFunction(files, 'test');
1720
+ expect(fn).toContain('{display:{Name:"Magic Wand"},Enchantments:[{id:"sharpness",lvl:5}]}');
1721
+ });
1722
+ it('compiles summon with NBT', () => {
1723
+ const src = `fn test() { summon("minecraft:zombie", 0, 64, 0, { CustomName: "Boss", NoAI: true }); }`;
1724
+ const files = compile(src, 'nbtsummon');
1725
+ const fn = getFunction(files, 'test');
1726
+ expect(fn).toContain('summon minecraft:zombie 0 64 0 {CustomName:"Boss",NoAI:1b}');
1727
+ });
1728
+ it('compiles give with bool values in NBT', () => {
1729
+ const src = `fn test() { give(@s, "minecraft:shield", 1, { Unbreakable: true }); }`;
1730
+ const files = compile(src, 'nbtbool');
1731
+ const fn = getFunction(files, 'test');
1732
+ expect(fn).toContain('{Unbreakable:1b}');
1733
+ });
1734
+ });
1735
+ // ---------------------------------------------------------------------------
1736
+ // Set Operations
1737
+ // ---------------------------------------------------------------------------
1738
+ describe('Set operations', () => {
1739
+ it('creates a new set', () => {
1740
+ const src = `fn test() { let s = set_new(); }`;
1741
+ const files = compile(src, 'settest');
1742
+ const fn = getFunction(files, 'test');
1743
+ expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
1744
+ });
1745
+ it('adds to a set with uniqueness check', () => {
1746
+ const src = `fn test() { let s = set_new(); set_add(s, "apple"); }`;
1747
+ const files = compile(src, 'setadd');
1748
+ const fn = getFunction(files, 'test');
1749
+ 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}');
1750
+ });
1751
+ it('checks set membership', () => {
1752
+ const src = `fn test() { let s = set_new(); set_add(s, "x"); let has = set_contains(s, "x"); }`;
1753
+ const files = compile(src, 'setcontains');
1754
+ const fn = getFunction(files, 'test');
1755
+ expect(fn).toContain('if data storage rs:sets __set_0[{value:x}]');
1756
+ });
1757
+ it('removes from a set', () => {
1758
+ const src = `fn test() { let s = set_new(); set_add(s, "y"); set_remove(s, "y"); }`;
1759
+ const files = compile(src, 'setremove');
1760
+ const fn = getFunction(files, 'test');
1761
+ expect(fn).toContain('data remove storage rs:sets __set_0[{value:y}]');
1762
+ });
1763
+ it('clears a set', () => {
1764
+ const src = `fn test() { let s = set_new(); set_clear(s); }`;
1765
+ const files = compile(src, 'setclear');
1766
+ const fn = getFunction(files, 'test');
1767
+ expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
1768
+ });
1769
+ });
1770
+ // ---------------------------------------------------------------------------
1771
+ // Method Syntax Sugar
1772
+ // ---------------------------------------------------------------------------
1773
+ describe('Method syntax sugar', () => {
1774
+ it('transforms obj.method() to method(obj)', () => {
1775
+ const src = `fn test() { let s = set_new(); s.clear(); }`;
1776
+ const files = compile(src, 'method1');
1777
+ const fn = getFunction(files, 'test');
1778
+ expect(fn).toContain('data modify storage rs:sets __set_0 set value []');
1779
+ });
1780
+ it('transforms obj.method(arg) to method(obj, arg)', () => {
1781
+ const src = `fn test() { let s = set_new(); s.add("apple"); }`;
1782
+ const files = compile(src, 'method2');
1783
+ const fn = getFunction(files, 'test');
1784
+ expect(fn).toContain('data modify storage rs:sets __set_0 append value {value:apple}');
1785
+ });
1786
+ it('transforms obj.method(arg) with contains', () => {
1787
+ const src = `fn test() { let s = set_new(); s.add("x"); let r = s.contains("x"); }`;
1788
+ const files = compile(src, 'method3');
1789
+ const fn = getFunction(files, 'test');
1790
+ expect(fn).toBeDefined();
1791
+ });
1792
+ it('works with multiple args', () => {
1793
+ const src = `fn test() { let s = set_new(); s.add("a"); s.add("b"); s.remove("a"); }`;
1794
+ const files = compile(src, 'method4');
1795
+ const fn = getFunction(files, 'test');
1796
+ expect(fn).toContain('data remove storage rs:sets __set_0[{value:a}]');
1797
+ });
1798
+ });
1799
+ describe('Global variables', () => {
1800
+ it('initializes global in __load', () => {
1801
+ const src = `let x: int = 42;\nfn test() { say("hi"); }`;
1802
+ const files = compile(src, 'globaltest');
1803
+ const load = getFunction(files, '__load');
1804
+ expect(load).toContain('scoreboard players set $x rs 42');
1805
+ });
1806
+ it('reads and writes global in function', () => {
1807
+ const src = `let count: int = 0;\nfn inc() { count = count + 1; }`;
1808
+ const files = compile(src, 'globalrw');
1809
+ const fn = getFunction(files, 'inc');
1810
+ expect(fn).toBeDefined();
1811
+ // Global should be initialized in __load
1812
+ const load = getFunction(files, '__load');
1813
+ expect(load).toContain('scoreboard players set $count rs 0');
1814
+ });
1815
+ it('const cannot be reassigned', () => {
1816
+ const src = `const X: int = 5;\nfn bad() { X = 10; }`;
1817
+ expect(() => compile(src, 'constbad')).toThrow();
1818
+ });
1819
+ });
1820
+ describe('@load decorator', () => {
1821
+ it('calls @load function from __load.mcfunction', () => {
1822
+ const src = `@load fn init() { say("Datapack loaded!"); }`;
1823
+ const files = compile(src, 'loadtest');
1824
+ const load = getFunction(files, '__load');
1825
+ expect(load).toContain('function loadtest:init');
1826
+ });
1827
+ it('calls multiple @load functions in order', () => {
1828
+ const src = `
1829
+ @load fn setup() { say("setup"); }
1830
+ @load fn init() { say("init"); }
1831
+ `;
1832
+ const files = compile(src, 'loadtest');
1833
+ const load = getFunction(files, '__load');
1834
+ const setupIdx = load.indexOf('function loadtest:setup');
1835
+ const initIdx = load.indexOf('function loadtest:init');
1836
+ expect(setupIdx).toBeGreaterThan(-1);
1837
+ expect(initIdx).toBeGreaterThan(-1);
1838
+ expect(setupIdx).toBeLessThan(initIdx);
1839
+ });
1840
+ it('generates the @load function body normally', () => {
1841
+ const src = `@load fn init() { say("hi"); }`;
1842
+ const files = compile(src, 'loadtest');
1843
+ const fn = getFunction(files, 'init');
1844
+ expect(fn).toContain('say hi');
1845
+ });
1846
+ });
1528
1847
  //# sourceMappingURL=e2e.test.js.map
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const formatter_1 = require("../formatter");
4
+ describe('formatter', () => {
5
+ it('normalizes indentation to 4 spaces', () => {
6
+ const input = 'fn main() {\n let x: int = 1;\n}';
7
+ const result = (0, formatter_1.format)(input);
8
+ expect(result).toBe('fn main() {\n let x: int = 1;\n}\n');
9
+ });
10
+ it('handles nested blocks', () => {
11
+ const input = 'fn main() {\nif true {\nlet x: int = 1;\n}\n}';
12
+ const result = (0, formatter_1.format)(input);
13
+ expect(result).toBe('fn main() {\n if true {\n let x: int = 1;\n }\n}\n');
14
+ });
15
+ it('trims trailing whitespace', () => {
16
+ const input = 'fn main() { \n let x: int = 1; \n} ';
17
+ const result = (0, formatter_1.format)(input);
18
+ expect(result).toBe('fn main() {\n let x: int = 1;\n}\n');
19
+ });
20
+ it('ensures single newline at end of file', () => {
21
+ const input = 'fn main() {\n}\n\n\n';
22
+ const result = (0, formatter_1.format)(input);
23
+ expect(result).toBe('fn main() {\n}\n');
24
+ });
25
+ it('preserves blank lines', () => {
26
+ const input = 'fn a() {\n}\n\nfn b() {\n}';
27
+ const result = (0, formatter_1.format)(input);
28
+ expect(result).toBe('fn a() {\n}\n\nfn b() {\n}\n');
29
+ });
30
+ it('handles already formatted code', () => {
31
+ const input = 'fn main() {\n let x: int = 1;\n}\n';
32
+ const result = (0, formatter_1.format)(input);
33
+ expect(result).toBe(input);
34
+ });
35
+ it('handles empty input', () => {
36
+ expect((0, formatter_1.format)('')).toBe('\n');
37
+ expect((0, formatter_1.format)('\n\n')).toBe('\n');
38
+ });
39
+ });
40
+ //# sourceMappingURL=formatter.test.js.map
@@ -10,10 +10,16 @@ function kinds(tokens) {
10
10
  describe('Lexer', () => {
11
11
  describe('keywords', () => {
12
12
  it('recognizes all keywords', () => {
13
- const tokens = tokenize('fn let const if else while for foreach match return as at in struct enum trigger namespace');
13
+ const tokens = tokenize('fn let const if else while for foreach match return as at in is struct impl enum trigger namespace');
14
14
  expect(kinds(tokens)).toEqual([
15
15
  'fn', 'let', 'const', 'if', 'else', 'while', 'for', 'foreach', 'match',
16
- 'return', 'as', 'at', 'in', 'struct', 'enum', 'trigger', 'namespace', 'eof'
16
+ 'return', 'as', 'at', 'in', 'is', 'struct', 'impl', 'enum', 'trigger', 'namespace', 'eof'
17
+ ]);
18
+ });
19
+ it('tokenizes is-check and impl syntax with their dedicated keywords', () => {
20
+ const tokens = tokenize('if (e is Player) { } impl Point { }');
21
+ expect(kinds(tokens)).toEqual([
22
+ 'if', '(', 'ident', 'is', 'ident', ')', '{', '}', 'impl', 'ident', '{', '}', 'eof',
17
23
  ]);
18
24
  });
19
25
  it('recognizes type keywords', () => {
@@ -169,6 +175,10 @@ describe('Lexer', () => {
169
175
  const tokens = tokenize('=>');
170
176
  expect(kinds(tokens)).toEqual(['=>', 'eof']);
171
177
  });
178
+ it('tokenizes static method separators for impl methods', () => {
179
+ const tokens = tokenize('Point::new()');
180
+ expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof']);
181
+ });
172
182
  });
173
183
  describe('delimiters', () => {
174
184
  it('tokenizes all delimiters', () => {