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
@@ -77,6 +77,41 @@ fn test() -> int {
77
77
  const call = getInstructions(specialized).find(i => i.op === 'call');
78
78
  expect(call.fn).toBe('__lambda_0');
79
79
  });
80
+ it('lowers impl methods to prefixed function names', () => {
81
+ const ir = compile(`
82
+ struct Timer { duration: int }
83
+
84
+ impl Timer {
85
+ fn elapsed(self) -> int {
86
+ return self.duration;
87
+ }
88
+ }
89
+ `);
90
+ expect(getFunction(ir, 'Timer_elapsed')).toBeDefined();
91
+ });
92
+ it('lowers impl instance and static method calls', () => {
93
+ const ir = compile(`
94
+ struct Timer { duration: int }
95
+
96
+ impl Timer {
97
+ fn new(duration: int) -> Timer {
98
+ return { duration: duration };
99
+ }
100
+
101
+ fn elapsed(self) -> int {
102
+ return self.duration;
103
+ }
104
+ }
105
+
106
+ fn test() -> int {
107
+ let timer: Timer = Timer::new(10);
108
+ return timer.elapsed();
109
+ }
110
+ `);
111
+ const fn = getFunction(ir, 'test');
112
+ const calls = getInstructions(fn).filter((instr) => instr.op === 'call');
113
+ expect(calls.map(call => call.fn)).toEqual(['Timer_new', 'Timer_elapsed']);
114
+ });
80
115
  });
81
116
  describe('let statements', () => {
82
117
  it('inlines const values without allocating scoreboard variables', () => {
@@ -204,6 +239,23 @@ fn test() -> int {
204
239
  const fn = getFunction(ir, 'foo');
205
240
  expect(fn.blocks.length).toBeGreaterThanOrEqual(3); // entry, then, else, merge
206
241
  });
242
+ it('lowers entity is-checks to execute if entity type filters', () => {
243
+ const ir = compile(`
244
+ fn scan() {
245
+ foreach (e in @e) {
246
+ if (e is Player) {
247
+ kill(e);
248
+ }
249
+ }
250
+ }
251
+ `);
252
+ const foreachFn = ir.functions.find(fn => fn.name.includes('scan/foreach'));
253
+ const rawCmds = getRawCommands(foreachFn);
254
+ const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=player] run function test:scan/then_'));
255
+ expect(isCheckCmd).toBeDefined();
256
+ const thenFn = ir.functions.find(fn => fn.name.startsWith('scan/then_'));
257
+ expect(getRawCommands(thenFn)).toContain('kill @s');
258
+ });
207
259
  });
208
260
  describe('while statements', () => {
209
261
  it('creates loop structure', () => {
@@ -243,6 +295,31 @@ fn test() -> int {
243
295
  const rawCmds = getRawCommands(fn);
244
296
  expect(rawCmds.some(cmd => cmd.includes('data get storage rs:heap arr'))).toBe(true);
245
297
  });
298
+ it('lowers entity is-checks inside foreach bodies', () => {
299
+ const ir = compile(`
300
+ fn test() {
301
+ foreach (e in @e) {
302
+ if (e is Player) {
303
+ give(@s, "diamond", 1);
304
+ }
305
+ if (e is Zombie) {
306
+ kill(@s);
307
+ }
308
+ }
309
+ }
310
+ `);
311
+ const mainFn = getFunction(ir, 'test');
312
+ const foreachFn = ir.functions.find(f => f.name === 'test/foreach_0');
313
+ const thenFns = ir.functions.filter(f => /^test\/then_/.test(f.name)).sort((a, b) => a.name.localeCompare(b.name));
314
+ const rawCmds = getRawCommands(foreachFn);
315
+ const [playerThenFn, zombieThenFn] = thenFns;
316
+ expect(getRawCommands(mainFn)).toContain('execute as @e run function test:test/foreach_0');
317
+ expect(thenFns).toHaveLength(2);
318
+ expect(rawCmds).toContain(`execute if entity @s[type=player] run function test:${playerThenFn.name}`);
319
+ expect(rawCmds).toContain(`execute if entity @s[type=zombie] run function test:${zombieThenFn.name}`);
320
+ expect(getRawCommands(playerThenFn).some(cmd => cmd.includes('give @s diamond 1'))).toBe(true);
321
+ expect(getRawCommands(zombieThenFn)).toContain('kill @s');
322
+ });
246
323
  });
247
324
  describe('match statements', () => {
248
325
  it('lowers match into guarded execute function calls', () => {
@@ -515,12 +592,12 @@ fn test() {
515
592
  `);
516
593
  const fn = getFunction(ir, 'test');
517
594
  const rawCmds = getRawCommands(fn);
518
- expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar kills');
519
- expect(rawCmds).toContain('scoreboard objectives setdisplay list coins');
520
- expect(rawCmds).toContain('scoreboard objectives setdisplay belowName hp');
595
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar test.kills');
596
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list test.coins');
597
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName test.hp');
521
598
  expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar');
522
- expect(rawCmds).toContain('scoreboard objectives add kills playerKillCount "Kill Count"');
523
- expect(rawCmds).toContain('scoreboard objectives remove kills');
599
+ expect(rawCmds).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
600
+ expect(rawCmds).toContain('scoreboard objectives remove test.kills');
524
601
  });
525
602
  it('lowers bossbar management builtins', () => {
526
603
  const ir = compile(`
@@ -546,7 +623,7 @@ fn test() {
546
623
  expect(rawCmds).toContain('bossbar set ns:health visible true');
547
624
  expect(rawCmds).toContain('bossbar set ns:health players @a');
548
625
  expect(rawCmds).toContain('bossbar remove ns:health');
549
- expect(rawCmds.some(cmd => /^execute store result score \$t\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true);
626
+ expect(rawCmds.some(cmd => /^execute store result score \$_\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true);
550
627
  });
551
628
  it('lowers team management builtins', () => {
552
629
  const ir = compile(`
@@ -574,13 +651,13 @@ fn test() {
574
651
  const ir = compile('fn test() { let x: int = random(1, 100); }');
575
652
  const fn = getFunction(ir, 'test');
576
653
  const rawCmds = getRawCommands(fn);
577
- expect(rawCmds).toContain('scoreboard players random $t0 rs 1 100');
654
+ expect(rawCmds).toContain('scoreboard players random $_0 rs 1 100');
578
655
  });
579
656
  it('lowers random_native()', () => {
580
657
  const ir = compile('fn test() { let x: int = random_native(1, 6); }');
581
658
  const fn = getFunction(ir, 'test');
582
659
  const rawCmds = getRawCommands(fn);
583
- expect(rawCmds).toContain('execute store result score $t0 rs run random value 1 6');
660
+ expect(rawCmds).toContain('execute store result score $_0 rs run random value 1 6');
584
661
  });
585
662
  it('lowers random_sequence()', () => {
586
663
  const ir = compile('fn test() { random_sequence("loot", 42); }');
@@ -588,6 +665,40 @@ fn test() {
588
665
  const rawCmds = getRawCommands(fn);
589
666
  expect(rawCmds).toContain('random reset loot 42');
590
667
  });
668
+ it('lowers setTimeout() to a scheduled helper function', () => {
669
+ const ir = compile('fn test() { setTimeout(100, () => { say("hi"); }); }');
670
+ const fn = getFunction(ir, 'test');
671
+ const timeoutFn = getFunction(ir, '__timeout_0');
672
+ const rawCmds = getRawCommands(fn);
673
+ const timeoutCmds = getRawCommands(timeoutFn);
674
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t');
675
+ expect(timeoutCmds).toContain('say hi');
676
+ });
677
+ it('lowers setInterval() to a self-rescheduling helper function', () => {
678
+ const ir = compile('fn test() { setInterval(20, () => { say("tick"); }); }');
679
+ const fn = getFunction(ir, 'test');
680
+ const intervalFn = getFunction(ir, '__interval_0');
681
+ const intervalBodyFn = getFunction(ir, '__interval_body_0');
682
+ const rawCmds = getRawCommands(fn);
683
+ const intervalCmds = getRawCommands(intervalFn);
684
+ const intervalBodyCmds = getRawCommands(intervalBodyFn);
685
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
686
+ expect(intervalCmds).toContain('function test:__interval_body_0');
687
+ expect(intervalCmds).toContain('schedule function test:__interval_0 20t');
688
+ expect(intervalBodyCmds).toContain('say tick');
689
+ });
690
+ it('lowers clearInterval() to schedule clear for the generated interval function', () => {
691
+ const ir = compile(`
692
+ fn test() {
693
+ let intervalId: int = setInterval(20, () => { say("tick"); });
694
+ clearInterval(intervalId);
695
+ }
696
+ `);
697
+ const fn = getFunction(ir, 'test');
698
+ const rawCmds = getRawCommands(fn);
699
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
700
+ expect(rawCmds).toContain('schedule clear test:__interval_0');
701
+ });
591
702
  it('lowers data_get from entity', () => {
592
703
  const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }');
593
704
  const fn = getFunction(ir, 'test');
@@ -628,19 +739,25 @@ fn test() {
628
739
  const ir = compile('fn test() { let score: int = scoreboard_get(@s, "score"); }');
629
740
  const fn = getFunction(ir, 'test');
630
741
  const rawCmds = getRawCommands(fn);
631
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s score'))).toBe(true);
742
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s test.score'))).toBe(true);
632
743
  });
633
744
  it('accepts bare selector targets in scoreboard_set', () => {
634
745
  const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }');
635
746
  const fn = getFunction(ir, 'test');
636
747
  const rawCmds = getRawCommands(fn);
637
- expect(rawCmds).toContain('scoreboard players set @a kills 0');
748
+ expect(rawCmds).toContain('scoreboard players set @a test.kills 0');
749
+ });
750
+ it('skips prefixing raw mc_name objectives', () => {
751
+ const ir = compile('fn test() { scoreboard_set(@s, #health, 100); }');
752
+ const fn = getFunction(ir, 'test');
753
+ const rawCmds = getRawCommands(fn);
754
+ expect(rawCmds).toContain('scoreboard players set @s health 100');
638
755
  });
639
756
  it('warns on quoted selectors in scoreboard_get', () => {
640
757
  const { ir, warnings } = compileWithWarnings('fn test() { let score: int = scoreboard_get("@s", "score"); }');
641
758
  const fn = getFunction(ir, 'test');
642
759
  const rawCmds = getRawCommands(fn);
643
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s score'))).toBe(true);
760
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s test.score'))).toBe(true);
644
761
  expect(warnings).toContainEqual(expect.objectContaining({
645
762
  code: 'W_QUOTED_SELECTOR',
646
763
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
@@ -650,7 +767,7 @@ fn test() {
650
767
  const { ir, warnings } = compileWithWarnings('fn test() { let total: int = scoreboard_get("#global", "total"); }');
651
768
  const fn = getFunction(ir, 'test');
652
769
  const rawCmds = getRawCommands(fn);
653
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get #global total'))).toBe(true);
770
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get #global test.total'))).toBe(true);
654
771
  expect(warnings).toHaveLength(0);
655
772
  });
656
773
  it('warns on quoted selectors in data_get entity targets', () => {
@@ -663,6 +780,37 @@ fn test() {
663
780
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
664
781
  }));
665
782
  });
783
+ it('keeps already-qualified scoreboard objectives unchanged', () => {
784
+ const ir = compile('fn test() { scoreboard_set(@s, "custom.timer", 5); }');
785
+ const fn = getFunction(ir, 'test');
786
+ const rawCmds = getRawCommands(fn);
787
+ expect(rawCmds).toContain('scoreboard players set @s custom.timer 5');
788
+ });
789
+ });
790
+ describe('timer builtins', () => {
791
+ it('lowers timer builtins into schedule commands and wrapper functions', () => {
792
+ const ir = compile(`
793
+ fn test() {
794
+ let intervalId: int = setInterval(20, () => {
795
+ say("tick");
796
+ });
797
+ setTimeout(100, () => {
798
+ say("later");
799
+ });
800
+ clearInterval(intervalId);
801
+ }
802
+ `);
803
+ const fn = getFunction(ir, 'test');
804
+ const rawCmds = getRawCommands(fn);
805
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
806
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t');
807
+ expect(rawCmds).toContain('schedule clear test:__interval_0');
808
+ const intervalFn = getFunction(ir, '__interval_0');
809
+ expect(getRawCommands(intervalFn)).toEqual([
810
+ 'function test:__interval_body_0',
811
+ 'schedule function test:__interval_0 20t',
812
+ ]);
813
+ });
666
814
  });
667
815
  describe('decorators', () => {
668
816
  it('marks @tick function', () => {
@@ -681,6 +829,13 @@ fn test() {
681
829
  const fn = getFunction(ir, 'handle_advancement');
682
830
  expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' });
683
831
  });
832
+ it('marks @on event functions and binds player to @s', () => {
833
+ const ir = compile('@on(PlayerDeath) fn handle_death(player: Player) { tp(player, @p); }');
834
+ const fn = getFunction(ir, 'handle_death');
835
+ expect(fn.eventHandler).toEqual({ eventType: 'PlayerDeath', tag: 'rs.just_died' });
836
+ expect(fn.params).toEqual([]);
837
+ expect(getRawCommands(fn)).toContain('tp @s @p');
838
+ });
684
839
  });
685
840
  describe('selectors', () => {
686
841
  it('converts selector with filters to string', () => {
@@ -815,5 +970,38 @@ fn count_down() {
815
970
  expect(bodyBlock).toBeDefined();
816
971
  });
817
972
  });
973
+ describe('Global variables', () => {
974
+ it('registers global in IR globals with init value', () => {
975
+ const ir = compile('let x: int = 42;\nfn test() { say("hi"); }');
976
+ expect(ir.globals).toContainEqual({ name: '$x', init: 42 });
977
+ });
978
+ it('reads global variable in function body', () => {
979
+ const ir = compile('let count: int = 0;\nfn test() { let y: int = count; }');
980
+ const fn = getFunction(ir, 'test');
981
+ const instrs = getInstructions(fn);
982
+ expect(instrs.some(i => i.op === 'assign' && i.dst === '$y' && i.src.kind === 'var' && i.src.name === '$count')).toBe(true);
983
+ });
984
+ it('writes global variable in function body', () => {
985
+ const ir = compile('let count: int = 0;\nfn inc() { count = 5; }');
986
+ const fn = getFunction(ir, 'inc');
987
+ const instrs = getInstructions(fn);
988
+ expect(instrs.some(i => i.op === 'assign' && i.dst === '$count' && i.src.kind === 'const' && i.src.value === 5)).toBe(true);
989
+ });
990
+ it('compound assignment on global variable', () => {
991
+ const ir = compile('let count: int = 0;\nfn inc() { count += 1; }');
992
+ const fn = getFunction(ir, 'inc');
993
+ const instrs = getInstructions(fn);
994
+ expect(instrs.some(i => i.op === 'binop' && i.lhs.name === '$count' && i.bop === '+' && i.rhs.value === 1)).toBe(true);
995
+ });
996
+ it('const cannot be reassigned', () => {
997
+ const src = 'const X: int = 5;\nfn bad() { X = 10; }';
998
+ expect(() => compile(src)).toThrow(/Cannot assign to constant/);
999
+ });
1000
+ it('multiple globals with different init values', () => {
1001
+ const ir = compile('let a: int = 10;\nlet b: int = 20;\nfn test() { a = b; }');
1002
+ expect(ir.globals).toContainEqual({ name: '$a', init: 10 });
1003
+ expect(ir.globals).toContainEqual({ name: '$b', init: 20 });
1004
+ });
1005
+ });
818
1006
  });
819
1007
  //# sourceMappingURL=lowering.test.js.map