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
@@ -88,6 +88,43 @@ fn test() -> int {
88
88
  const call = getInstructions(specialized!).find(i => i.op === 'call') as any
89
89
  expect(call.fn).toBe('__lambda_0')
90
90
  })
91
+
92
+ it('lowers impl methods to prefixed function names', () => {
93
+ const ir = compile(`
94
+ struct Timer { duration: int }
95
+
96
+ impl Timer {
97
+ fn elapsed(self) -> int {
98
+ return self.duration;
99
+ }
100
+ }
101
+ `)
102
+ expect(getFunction(ir, 'Timer_elapsed')).toBeDefined()
103
+ })
104
+
105
+ it('lowers impl instance and static method calls', () => {
106
+ const ir = compile(`
107
+ struct Timer { duration: int }
108
+
109
+ impl Timer {
110
+ fn new(duration: int) -> Timer {
111
+ return { duration: duration };
112
+ }
113
+
114
+ fn elapsed(self) -> int {
115
+ return self.duration;
116
+ }
117
+ }
118
+
119
+ fn test() -> int {
120
+ let timer: Timer = Timer::new(10);
121
+ return timer.elapsed();
122
+ }
123
+ `)
124
+ const fn = getFunction(ir, 'test')!
125
+ const calls = getInstructions(fn).filter((instr): instr is IRInstr & { op: 'call' } => instr.op === 'call')
126
+ expect(calls.map(call => call.fn)).toEqual(['Timer_new', 'Timer_elapsed'])
127
+ })
91
128
  })
92
129
 
93
130
  describe('let statements', () => {
@@ -235,6 +272,25 @@ fn test() -> int {
235
272
  const fn = getFunction(ir, 'foo')!
236
273
  expect(fn.blocks.length).toBeGreaterThanOrEqual(3) // entry, then, else, merge
237
274
  })
275
+
276
+ it('lowers entity is-checks to execute if entity type filters', () => {
277
+ const ir = compile(`
278
+ fn scan() {
279
+ foreach (e in @e) {
280
+ if (e is Player) {
281
+ kill(e);
282
+ }
283
+ }
284
+ }
285
+ `)
286
+ const foreachFn = ir.functions.find(fn => fn.name.includes('scan/foreach'))!
287
+ const rawCmds = getRawCommands(foreachFn)
288
+ const isCheckCmd = rawCmds.find(cmd => cmd.startsWith('execute if entity @s[type=player] run function test:scan/then_'))
289
+ expect(isCheckCmd).toBeDefined()
290
+
291
+ const thenFn = ir.functions.find(fn => fn.name.startsWith('scan/then_'))!
292
+ expect(getRawCommands(thenFn)).toContain('kill @s')
293
+ })
238
294
  })
239
295
 
240
296
  describe('while statements', () => {
@@ -282,6 +338,33 @@ fn test() -> int {
282
338
  const rawCmds = getRawCommands(fn)
283
339
  expect(rawCmds.some(cmd => cmd.includes('data get storage rs:heap arr'))).toBe(true)
284
340
  })
341
+
342
+ it('lowers entity is-checks inside foreach bodies', () => {
343
+ const ir = compile(`
344
+ fn test() {
345
+ foreach (e in @e) {
346
+ if (e is Player) {
347
+ give(@s, "diamond", 1);
348
+ }
349
+ if (e is Zombie) {
350
+ kill(@s);
351
+ }
352
+ }
353
+ }
354
+ `)
355
+ const mainFn = getFunction(ir, 'test')!
356
+ const foreachFn = ir.functions.find(f => f.name === 'test/foreach_0')!
357
+ const thenFns = ir.functions.filter(f => /^test\/then_/.test(f.name)).sort((a, b) => a.name.localeCompare(b.name))
358
+ const rawCmds = getRawCommands(foreachFn)
359
+ const [playerThenFn, zombieThenFn] = thenFns
360
+
361
+ expect(getRawCommands(mainFn)).toContain('execute as @e run function test:test/foreach_0')
362
+ expect(thenFns).toHaveLength(2)
363
+ expect(rawCmds).toContain(`execute if entity @s[type=player] run function test:${playerThenFn.name}`)
364
+ expect(rawCmds).toContain(`execute if entity @s[type=zombie] run function test:${zombieThenFn.name}`)
365
+ expect(getRawCommands(playerThenFn).some(cmd => cmd.includes('give @s diamond 1'))).toBe(true)
366
+ expect(getRawCommands(zombieThenFn)).toContain('kill @s')
367
+ })
285
368
  })
286
369
 
287
370
  describe('match statements', () => {
@@ -603,12 +686,12 @@ fn test() {
603
686
  `)
604
687
  const fn = getFunction(ir, 'test')!
605
688
  const rawCmds = getRawCommands(fn)
606
- expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar kills')
607
- expect(rawCmds).toContain('scoreboard objectives setdisplay list coins')
608
- expect(rawCmds).toContain('scoreboard objectives setdisplay belowName hp')
689
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar test.kills')
690
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list test.coins')
691
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName test.hp')
609
692
  expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar')
610
- expect(rawCmds).toContain('scoreboard objectives add kills playerKillCount "Kill Count"')
611
- expect(rawCmds).toContain('scoreboard objectives remove kills')
693
+ expect(rawCmds).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"')
694
+ expect(rawCmds).toContain('scoreboard objectives remove test.kills')
612
695
  })
613
696
 
614
697
  it('lowers bossbar management builtins', () => {
@@ -635,7 +718,7 @@ fn test() {
635
718
  expect(rawCmds).toContain('bossbar set ns:health visible true')
636
719
  expect(rawCmds).toContain('bossbar set ns:health players @a')
637
720
  expect(rawCmds).toContain('bossbar remove ns:health')
638
- expect(rawCmds.some(cmd => /^execute store result score \$t\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true)
721
+ expect(rawCmds.some(cmd => /^execute store result score \$_\d+ rs run bossbar get ns:health value$/.test(cmd))).toBe(true)
639
722
  })
640
723
 
641
724
  it('lowers team management builtins', () => {
@@ -665,14 +748,14 @@ fn test() {
665
748
  const ir = compile('fn test() { let x: int = random(1, 100); }')
666
749
  const fn = getFunction(ir, 'test')!
667
750
  const rawCmds = getRawCommands(fn)
668
- expect(rawCmds).toContain('scoreboard players random $t0 rs 1 100')
751
+ expect(rawCmds).toContain('scoreboard players random $_0 rs 1 100')
669
752
  })
670
753
 
671
754
  it('lowers random_native()', () => {
672
755
  const ir = compile('fn test() { let x: int = random_native(1, 6); }')
673
756
  const fn = getFunction(ir, 'test')!
674
757
  const rawCmds = getRawCommands(fn)
675
- expect(rawCmds).toContain('execute store result score $t0 rs run random value 1 6')
758
+ expect(rawCmds).toContain('execute store result score $_0 rs run random value 1 6')
676
759
  })
677
760
 
678
761
  it('lowers random_sequence()', () => {
@@ -682,6 +765,43 @@ fn test() {
682
765
  expect(rawCmds).toContain('random reset loot 42')
683
766
  })
684
767
 
768
+ it('lowers setTimeout() to a scheduled helper function', () => {
769
+ const ir = compile('fn test() { setTimeout(100, () => { say("hi"); }); }')
770
+ const fn = getFunction(ir, 'test')!
771
+ const timeoutFn = getFunction(ir, '__timeout_0')!
772
+ const rawCmds = getRawCommands(fn)
773
+ const timeoutCmds = getRawCommands(timeoutFn)
774
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t')
775
+ expect(timeoutCmds).toContain('say hi')
776
+ })
777
+
778
+ it('lowers setInterval() to a self-rescheduling helper function', () => {
779
+ const ir = compile('fn test() { setInterval(20, () => { say("tick"); }); }')
780
+ const fn = getFunction(ir, 'test')!
781
+ const intervalFn = getFunction(ir, '__interval_0')!
782
+ const intervalBodyFn = getFunction(ir, '__interval_body_0')!
783
+ const rawCmds = getRawCommands(fn)
784
+ const intervalCmds = getRawCommands(intervalFn)
785
+ const intervalBodyCmds = getRawCommands(intervalBodyFn)
786
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
787
+ expect(intervalCmds).toContain('function test:__interval_body_0')
788
+ expect(intervalCmds).toContain('schedule function test:__interval_0 20t')
789
+ expect(intervalBodyCmds).toContain('say tick')
790
+ })
791
+
792
+ it('lowers clearInterval() to schedule clear for the generated interval function', () => {
793
+ const ir = compile(`
794
+ fn test() {
795
+ let intervalId: int = setInterval(20, () => { say("tick"); });
796
+ clearInterval(intervalId);
797
+ }
798
+ `)
799
+ const fn = getFunction(ir, 'test')!
800
+ const rawCmds = getRawCommands(fn)
801
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
802
+ expect(rawCmds).toContain('schedule clear test:__interval_0')
803
+ })
804
+
685
805
  it('lowers data_get from entity', () => {
686
806
  const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }')
687
807
  const fn = getFunction(ir, 'test')!
@@ -736,7 +856,7 @@ fn test() {
736
856
  const fn = getFunction(ir, 'test')!
737
857
  const rawCmds = getRawCommands(fn)
738
858
  expect(rawCmds.some(cmd =>
739
- cmd.includes('run scoreboard players get @s score')
859
+ cmd.includes('run scoreboard players get @s test.score')
740
860
  )).toBe(true)
741
861
  })
742
862
 
@@ -744,7 +864,14 @@ fn test() {
744
864
  const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }')
745
865
  const fn = getFunction(ir, 'test')!
746
866
  const rawCmds = getRawCommands(fn)
747
- expect(rawCmds).toContain('scoreboard players set @a kills 0')
867
+ expect(rawCmds).toContain('scoreboard players set @a test.kills 0')
868
+ })
869
+
870
+ it('skips prefixing raw mc_name objectives', () => {
871
+ const ir = compile('fn test() { scoreboard_set(@s, #health, 100); }')
872
+ const fn = getFunction(ir, 'test')!
873
+ const rawCmds = getRawCommands(fn)
874
+ expect(rawCmds).toContain('scoreboard players set @s health 100')
748
875
  })
749
876
 
750
877
  it('warns on quoted selectors in scoreboard_get', () => {
@@ -752,7 +879,7 @@ fn test() {
752
879
  const fn = getFunction(ir, 'test')!
753
880
  const rawCmds = getRawCommands(fn)
754
881
  expect(rawCmds.some(cmd =>
755
- cmd.includes('run scoreboard players get @s score')
882
+ cmd.includes('run scoreboard players get @s test.score')
756
883
  )).toBe(true)
757
884
  expect(warnings).toContainEqual(expect.objectContaining({
758
885
  code: 'W_QUOTED_SELECTOR',
@@ -765,7 +892,7 @@ fn test() {
765
892
  const fn = getFunction(ir, 'test')!
766
893
  const rawCmds = getRawCommands(fn)
767
894
  expect(rawCmds.some(cmd =>
768
- cmd.includes('run scoreboard players get #global total')
895
+ cmd.includes('run scoreboard players get #global test.total')
769
896
  )).toBe(true)
770
897
  expect(warnings).toHaveLength(0)
771
898
  })
@@ -782,6 +909,40 @@ fn test() {
782
909
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
783
910
  }))
784
911
  })
912
+
913
+ it('keeps already-qualified scoreboard objectives unchanged', () => {
914
+ const ir = compile('fn test() { scoreboard_set(@s, "custom.timer", 5); }')
915
+ const fn = getFunction(ir, 'test')!
916
+ const rawCmds = getRawCommands(fn)
917
+ expect(rawCmds).toContain('scoreboard players set @s custom.timer 5')
918
+ })
919
+ })
920
+
921
+ describe('timer builtins', () => {
922
+ it('lowers timer builtins into schedule commands and wrapper functions', () => {
923
+ const ir = compile(`
924
+ fn test() {
925
+ let intervalId: int = setInterval(20, () => {
926
+ say("tick");
927
+ });
928
+ setTimeout(100, () => {
929
+ say("later");
930
+ });
931
+ clearInterval(intervalId);
932
+ }
933
+ `)
934
+ const fn = getFunction(ir, 'test')!
935
+ const rawCmds = getRawCommands(fn)
936
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t')
937
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t')
938
+ expect(rawCmds).toContain('schedule clear test:__interval_0')
939
+
940
+ const intervalFn = getFunction(ir, '__interval_0')!
941
+ expect(getRawCommands(intervalFn)).toEqual([
942
+ 'function test:__interval_body_0',
943
+ 'schedule function test:__interval_0 20t',
944
+ ])
945
+ })
785
946
  })
786
947
 
787
948
  describe('decorators', () => {
@@ -803,6 +964,14 @@ fn test() {
803
964
  const fn = getFunction(ir, 'handle_advancement')!
804
965
  expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' })
805
966
  })
967
+
968
+ it('marks @on event functions and binds player to @s', () => {
969
+ const ir = compile('@on(PlayerDeath) fn handle_death(player: Player) { tp(player, @p); }')
970
+ const fn = getFunction(ir, 'handle_death')!
971
+ expect(fn.eventHandler).toEqual({ eventType: 'PlayerDeath', tag: 'rs.just_died' })
972
+ expect(fn.params).toEqual([])
973
+ expect(getRawCommands(fn)).toContain('tp @s @p')
974
+ })
806
975
  })
807
976
 
808
977
  describe('selectors', () => {
@@ -959,4 +1128,49 @@ fn count_down() {
959
1128
  expect(bodyBlock).toBeDefined()
960
1129
  })
961
1130
  })
1131
+
1132
+ describe('Global variables', () => {
1133
+ it('registers global in IR globals with init value', () => {
1134
+ const ir = compile('let x: int = 42;\nfn test() { say("hi"); }')
1135
+ expect(ir.globals).toContainEqual({ name: '$x', init: 42 })
1136
+ })
1137
+
1138
+ it('reads global variable in function body', () => {
1139
+ const ir = compile('let count: int = 0;\nfn test() { let y: int = count; }')
1140
+ const fn = getFunction(ir, 'test')!
1141
+ const instrs = getInstructions(fn)
1142
+ expect(instrs.some(i =>
1143
+ i.op === 'assign' && i.dst === '$y' && (i.src as any).kind === 'var' && (i.src as any).name === '$count'
1144
+ )).toBe(true)
1145
+ })
1146
+
1147
+ it('writes global variable in function body', () => {
1148
+ const ir = compile('let count: int = 0;\nfn inc() { count = 5; }')
1149
+ const fn = getFunction(ir, 'inc')!
1150
+ const instrs = getInstructions(fn)
1151
+ expect(instrs.some(i =>
1152
+ i.op === 'assign' && i.dst === '$count' && (i.src as any).kind === 'const' && (i.src as any).value === 5
1153
+ )).toBe(true)
1154
+ })
1155
+
1156
+ it('compound assignment on global variable', () => {
1157
+ const ir = compile('let count: int = 0;\nfn inc() { count += 1; }')
1158
+ const fn = getFunction(ir, 'inc')!
1159
+ const instrs = getInstructions(fn)
1160
+ expect(instrs.some(i =>
1161
+ i.op === 'binop' && (i.lhs as any).name === '$count' && i.bop === '+' && (i.rhs as any).value === 1
1162
+ )).toBe(true)
1163
+ })
1164
+
1165
+ it('const cannot be reassigned', () => {
1166
+ const src = 'const X: int = 5;\nfn bad() { X = 10; }'
1167
+ expect(() => compile(src)).toThrow(/Cannot assign to constant/)
1168
+ })
1169
+
1170
+ it('multiple globals with different init values', () => {
1171
+ const ir = compile('let a: int = 10;\nlet b: int = 20;\nfn test() { a = b; }')
1172
+ expect(ir.globals).toContainEqual({ name: '$a', init: 10 })
1173
+ expect(ir.globals).toContainEqual({ name: '$b', init: 20 })
1174
+ })
1175
+ })
962
1176
  })