redscript-mc 1.1.0 → 1.2.1

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 (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. package/src/typechecker/index.ts +469 -18
@@ -146,6 +146,195 @@ fn main() {
146
146
  })
147
147
  })
148
148
 
149
+ describe('timer builtins', () => {
150
+ it('generates scheduled timer helper functions', () => {
151
+ const files = compile(`
152
+ fn main() {
153
+ let intervalId: int = setInterval(20, () => {
154
+ say("tick");
155
+ });
156
+ setTimeout(100, () => {
157
+ say("later");
158
+ });
159
+ clearInterval(intervalId);
160
+ }
161
+ `)
162
+ const mainFn = getFunction(files, 'main')
163
+ const intervalFn = getFunction(files, '__interval_0')
164
+ const intervalBodyFn = getFunction(files, '__interval_body_0')
165
+ const timeoutFn = getFunction(files, '__timeout_0')
166
+ expect(mainFn).toBeDefined()
167
+ expect(mainFn).toContain('schedule function test:__interval_0 20t')
168
+ expect(mainFn).toContain('schedule function test:__timeout_0 100t')
169
+ expect(mainFn).toContain('schedule clear test:__interval_0')
170
+ expect(intervalFn).toContain('function test:__interval_body_0')
171
+ expect(intervalFn).toContain('schedule function test:__interval_0 20t')
172
+ expect(intervalBodyFn).toContain('say tick')
173
+ expect(timeoutFn).toContain('say later')
174
+ })
175
+ })
176
+
177
+ describe('is type narrowing', () => {
178
+ it('type checks and compiles entity narrowing inside foreach blocks', () => {
179
+ const source = `
180
+ fn main() {
181
+ foreach (e in @e) {
182
+ if (e is Player) {
183
+ kill(e);
184
+ }
185
+ if (e is Zombie) {
186
+ kill(e);
187
+ }
188
+ }
189
+ }
190
+ `
191
+ expect(typeCheck(source)).toEqual([])
192
+
193
+ const files = compile(source)
194
+ const mainFn = getFunction(files, 'main')
195
+ const foreachFn = getSubFunction(files, 'main', 'foreach_0')
196
+ const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'))
197
+
198
+ expect(mainFn).toContain('execute as @e run function test:main/foreach_0')
199
+ expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_')
200
+ expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_')
201
+ expect(thenFiles).toHaveLength(2)
202
+ })
203
+ })
204
+
205
+ describe('impl blocks', () => {
206
+ it('compiles static and instance impl methods end to end', () => {
207
+ const source = `
208
+ struct Point { x: int, y: int }
209
+
210
+ impl Point {
211
+ fn new(x: int, y: int) -> Point {
212
+ return { x: x, y: y };
213
+ }
214
+
215
+ fn distance(self) -> int {
216
+ return self.x + self.y;
217
+ }
218
+ }
219
+
220
+ fn main() {
221
+ let p: Point = Point::new(1, 2);
222
+ let d: int = p.distance();
223
+ say("\${d}");
224
+ }
225
+ `
226
+ expect(typeCheck(source)).toEqual([])
227
+
228
+ const files = compile(source)
229
+ const mainFn = getFunction(files, 'main')
230
+ const staticFn = getFunction(files, 'Point_new')
231
+ const instanceFn = getFunction(files, 'Point_distance')
232
+
233
+ expect(mainFn).toContain('function test:Point_new')
234
+ expect(mainFn).toContain('function test:Point_distance')
235
+ expect(staticFn).toBeDefined()
236
+ expect(instanceFn).toBeDefined()
237
+ })
238
+ })
239
+
240
+ describe('namespace prefixing', () => {
241
+ it('prefixes user objectives but preserves mc_name and qualified objectives', () => {
242
+ const files = compile(`
243
+ fn main() {
244
+ scoreboard_set("timer", #rs, 100);
245
+ scoreboard_set(@s, "timer", 100);
246
+ scoreboard_set(@s, #health, 20);
247
+ scoreboard_set(@s, "custom.timer", 1);
248
+ }
249
+ `, 'pack')
250
+ const mainFn = getFunction(files, 'main')
251
+ expect(mainFn).toContain('scoreboard players set timer rs 100')
252
+ expect(mainFn).toContain('scoreboard players set @s pack.timer 100')
253
+ expect(mainFn).toContain('scoreboard players set @s health 20')
254
+ expect(mainFn).toContain('scoreboard players set @s custom.timer 1')
255
+ })
256
+ })
257
+
258
+ describe('Timer OOP API', () => {
259
+ it('compiles the Timer impl API end to end', () => {
260
+ const source = `
261
+ struct Timer {
262
+ _id: int,
263
+ _duration: int
264
+ }
265
+
266
+ impl Timer {
267
+ fn new(duration: int) -> Timer {
268
+ scoreboard_set("timer_ticks", #rs, 0);
269
+ scoreboard_set("timer_active", #rs, 0);
270
+ return { _id: 0, _duration: duration };
271
+ }
272
+
273
+ fn start(self) {
274
+ scoreboard_set("timer_active", #rs, 1);
275
+ }
276
+
277
+ fn pause(self) {
278
+ scoreboard_set("timer_active", #rs, 0);
279
+ }
280
+
281
+ fn reset(self) {
282
+ scoreboard_set("timer_ticks", #rs, 0);
283
+ }
284
+
285
+ fn done(self) -> bool {
286
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
287
+ return ticks >= self._duration;
288
+ }
289
+
290
+ fn tick(self) {
291
+ let active: int = scoreboard_get("timer_active", #rs);
292
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
293
+
294
+ if (active == 1) {
295
+ if (ticks < self._duration) {
296
+ scoreboard_set("timer_ticks", #rs, ticks + 1);
297
+ }
298
+ }
299
+ }
300
+ }
301
+
302
+ fn main() {
303
+ let t: Timer = Timer::new(100);
304
+ t.start();
305
+ t.tick();
306
+ let finished: bool = t.done();
307
+ if (finished) {
308
+ say("done");
309
+ }
310
+ t.pause();
311
+ t.reset();
312
+ }
313
+ `
314
+ expect(typeCheck(source)).toEqual([])
315
+
316
+ const files = compile(source, 'timerapi')
317
+ const mainFn = getFunction(files, 'main')
318
+ const newFn = getFunction(files, 'Timer_new')
319
+ const startFn = getFunction(files, 'Timer_start')
320
+ const tickFn = getFunction(files, 'Timer_tick')
321
+ const doneFn = getFunction(files, 'Timer_done')
322
+ const pauseFn = getFunction(files, 'Timer_pause')
323
+ const resetFn = getFunction(files, 'Timer_reset')
324
+
325
+ expect(mainFn).toContain('function timerapi:Timer_new')
326
+ expect(mainFn).toContain('function timerapi:Timer_start')
327
+ expect(mainFn).toContain('function timerapi:Timer_tick')
328
+ expect(mainFn).toContain('function timerapi:Timer_done')
329
+ expect(newFn).toContain('scoreboard players set timer_ticks rs 0')
330
+ expect(startFn).toContain('scoreboard players set timer_active rs 1')
331
+ expect(tickFn).toContain('scoreboard players get timer_active rs')
332
+ expect(doneFn).toContain('scoreboard players get timer_ticks rs')
333
+ expect(pauseFn).toContain('scoreboard players set timer_active rs 0')
334
+ expect(resetFn).toContain('scoreboard players set timer_ticks rs 0')
335
+ })
336
+ })
337
+
149
338
  describe('advancement event decorators', () => {
150
339
  it('generates advancement json with reward function path', () => {
151
340
  const source = `
@@ -309,12 +498,12 @@ fn test() {
309
498
  }
310
499
  `
311
500
  const fn = getFunction(compile(source), 'test')!
312
- expect(fn).toContain('scoreboard objectives setdisplay sidebar kills')
313
- expect(fn).toContain('scoreboard objectives setdisplay list coins')
314
- expect(fn).toContain('scoreboard objectives setdisplay belowName hp')
501
+ expect(fn).toContain('scoreboard objectives setdisplay sidebar test.kills')
502
+ expect(fn).toContain('scoreboard objectives setdisplay list test.coins')
503
+ expect(fn).toContain('scoreboard objectives setdisplay belowName test.hp')
315
504
  expect(fn).toContain('scoreboard objectives setdisplay sidebar')
316
- expect(fn).toContain('scoreboard objectives add kills playerKillCount "Kill Count"')
317
- expect(fn).toContain('scoreboard objectives remove kills')
505
+ expect(fn).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"')
506
+ expect(fn).toContain('scoreboard objectives remove test.kills')
318
507
  })
319
508
 
320
509
  it('compiles bossbar builtins', () => {
@@ -536,7 +725,7 @@ fn test() -> int {
536
725
  const fn = getFunction(files, 'test')
537
726
  expect(fn).toBeDefined()
538
727
  expect(fn).toContain('execute store result score')
539
- expect(fn).toContain('scoreboard players get PlayerName kill_count')
728
+ expect(fn).toContain('scoreboard players get PlayerName test.kill_count')
540
729
  })
541
730
 
542
731
  it('compiles scoreboard_get with @s selector', () => {
@@ -549,7 +738,7 @@ fn test() -> int {
549
738
  const files = compile(source)
550
739
  const fn = getFunction(files, 'test')
551
740
  expect(fn).toBeDefined()
552
- expect(fn).toContain('scoreboard players get @s kill_count')
741
+ expect(fn).toContain('scoreboard players get @s test.kill_count')
553
742
  })
554
743
 
555
744
  it('compiles scoreboard_set with constant value', () => {
@@ -561,7 +750,7 @@ fn test() {
561
750
  const files = compile(source)
562
751
  const fn = getFunction(files, 'test')
563
752
  expect(fn).toBeDefined()
564
- expect(fn).toContain('scoreboard players set PlayerName kill_count 100')
753
+ expect(fn).toContain('scoreboard players set PlayerName test.kill_count 100')
565
754
  })
566
755
 
567
756
  it('compiles scoreboard_set with variable value', () => {
@@ -576,7 +765,7 @@ fn test() {
576
765
  .filter(f => f.path.includes('test'))
577
766
  .map(f => f.content)
578
767
  .join('\n')
579
- expect(allContent).toContain('execute store result score @s score')
768
+ expect(allContent).toContain('execute store result score @s test.score')
580
769
  })
581
770
 
582
771
  it('compiles score() as expression', () => {
@@ -604,7 +793,7 @@ fn double_score() -> int {
604
793
  const files = compile(source)
605
794
  const fn = getFunction(files, 'double_score')
606
795
  expect(fn).toBeDefined()
607
- expect(fn).toContain('scoreboard players get @s points')
796
+ expect(fn).toContain('scoreboard players get @s test.points')
608
797
  })
609
798
  })
610
799
 
@@ -1481,10 +1670,10 @@ fn heal(amount: int) {
1481
1670
 
1482
1671
  describe('backward compat: string objective still works', () => {
1483
1672
  const source = `fn test() { let x: int = scoreboard_get(@s, "kills"); }`
1484
- it('compiles "kills" string to bare objective name', () => {
1673
+ it('prefixes plain string objectives with the active namespace', () => {
1485
1674
  const files = compile(source, 'compat')
1486
1675
  const fn = getFunction(files, 'test')
1487
- expect(fn).toContain('scoreboard players get @s kills')
1676
+ expect(fn).toContain('scoreboard players get @s compat.kills')
1488
1677
  })
1489
1678
  })
1490
1679
 
@@ -0,0 +1,13 @@
1
+ // Test @on(PlayerDeath) event system
2
+ // Since we can't easily kill players in test, we manually set the tag
3
+
4
+ @on(PlayerDeath)
5
+ fn handle_death(player: Player) {
6
+ say("Player died event triggered!");
7
+ scoreboard_set("#event_test", "death_count", scoreboard_get("#event_test", "death_count") + 1);
8
+ }
9
+
10
+ fn trigger_fake_death() {
11
+ // Manually add the death tag to simulate event
12
+ tag_add(@a, "rs.just_died");
13
+ }
@@ -0,0 +1,46 @@
1
+ struct timer {
2
+ _id: int,
3
+ _duration: int
4
+ }
5
+
6
+ impl timer {
7
+ fn new(duration: int) -> timer {
8
+ scoreboard_set("timer_ticks", #rs, 0);
9
+ scoreboard_set("timer_active", #rs, 0);
10
+ return { _id: 0, _duration: duration };
11
+ }
12
+
13
+ fn start(self) {
14
+ scoreboard_set("timer_active", #rs, 1);
15
+ }
16
+
17
+ fn tick(self) {
18
+ let active: int = scoreboard_get("timer_active", #rs);
19
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
20
+
21
+ if (active == 1) {
22
+ if (ticks < self._duration) {
23
+ scoreboard_set("timer_ticks", #rs, ticks + 1);
24
+ }
25
+ }
26
+ }
27
+
28
+ fn done(self) -> bool {
29
+ let ticks: int = scoreboard_get("timer_ticks", #rs);
30
+ return ticks >= 3;
31
+ }
32
+ }
33
+
34
+ fn test() {
35
+ let timer_value: timer = timer::new(3);
36
+ timer_value.start();
37
+ timer_value.tick();
38
+ timer_value.tick();
39
+ timer_value.tick();
40
+ let finished: bool = timer_value.done();
41
+ if (finished) {
42
+ scoreboard_set("#impl", #done, 1);
43
+ return;
44
+ }
45
+ scoreboard_set("#impl", #done, 1);
46
+ }
@@ -0,0 +1,11 @@
1
+ fn start() {
2
+ scoreboard_set("#interval", #ticks, 0);
3
+
4
+ setInterval(20, () => {
5
+ let count: int = scoreboard_get("#interval", #ticks);
6
+ if (count < 3) {
7
+ say("Interval tick");
8
+ scoreboard_set("#interval", #ticks, count + 1);
9
+ }
10
+ });
11
+ }
@@ -0,0 +1,20 @@
1
+ fn check_types() {
2
+ scoreboard_set("#is_check", #players, 0);
3
+ scoreboard_set("#is_check", #zombies, 0);
4
+
5
+ foreach (e in @e[type=zombie,tag=is_check_target]) {
6
+ if (e is Player) {
7
+ let players: int = scoreboard_get("#is_check", #players);
8
+ scoreboard_set("#is_check", #players, players + 1);
9
+ }
10
+
11
+ if (e is Zombie) {
12
+ let players: int = scoreboard_get("#is_check", #players);
13
+ scoreboard_set("#is_check", #players, players);
14
+ }
15
+
16
+ let zombies: int = scoreboard_get("#is_check", #zombies);
17
+ scoreboard_set("#is_check", #zombies, zombies + 1);
18
+ kill(e);
19
+ }
20
+ }
@@ -0,0 +1,7 @@
1
+ fn start() {
2
+ scoreboard_set("#timeout", #fired, 0);
3
+ setTimeout(20, () => {
4
+ say("Delayed message");
5
+ scoreboard_set("#timeout", #fired, 1);
6
+ });
7
+ }
@@ -11,10 +11,17 @@ function kinds(tokens: Token[]): TokenKind[] {
11
11
  describe('Lexer', () => {
12
12
  describe('keywords', () => {
13
13
  it('recognizes all keywords', () => {
14
- const tokens = tokenize('fn let const if else while for foreach match return as at in struct enum trigger namespace')
14
+ const tokens = tokenize('fn let const if else while for foreach match return as at in is struct impl enum trigger namespace')
15
15
  expect(kinds(tokens)).toEqual([
16
16
  'fn', 'let', 'const', 'if', 'else', 'while', 'for', 'foreach', 'match',
17
- 'return', 'as', 'at', 'in', 'struct', 'enum', 'trigger', 'namespace', 'eof'
17
+ 'return', 'as', 'at', 'in', 'is', 'struct', 'impl', 'enum', 'trigger', 'namespace', 'eof'
18
+ ])
19
+ })
20
+
21
+ it('tokenizes is-check and impl syntax with their dedicated keywords', () => {
22
+ const tokens = tokenize('if (e is Player) { } impl Point { }')
23
+ expect(kinds(tokens)).toEqual([
24
+ 'if', '(', 'ident', 'is', 'ident', ')', '{', '}', 'impl', 'ident', '{', '}', 'eof',
18
25
  ])
19
26
  })
20
27
 
@@ -81,6 +88,14 @@ describe('Lexer', () => {
81
88
  ])
82
89
  })
83
90
 
91
+ it('tokenizes f-strings as a dedicated token', () => {
92
+ const tokens = tokenize('f"Hello {name}!"')
93
+ expect(tokens.map(t => [t.kind, t.value])).toEqual([
94
+ ['f_string', 'Hello {name}!'],
95
+ ['eof', ''],
96
+ ])
97
+ })
98
+
84
99
  it('tokenizes byte literals (b suffix)', () => {
85
100
  const tokens = tokenize('20b 0B 127b')
86
101
  expect(tokens.map(t => [t.kind, t.value])).toEqual([
@@ -163,8 +178,19 @@ describe('Lexer', () => {
163
178
 
164
179
  describe('operators', () => {
165
180
  it('tokenizes arithmetic operators', () => {
166
- const tokens = tokenize('+ - * / % ~ ^')
167
- expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', '~', '^', 'eof'])
181
+ const tokens = tokenize('+ - * / %')
182
+ expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', 'eof'])
183
+ })
184
+
185
+ it('tokenizes relative and local coordinates', () => {
186
+ const tokens = tokenize('~ ~5 ~-3 ^ ^10 ^-2')
187
+ expect(kinds(tokens)).toEqual(['rel_coord', 'rel_coord', 'rel_coord', 'local_coord', 'local_coord', 'local_coord', 'eof'])
188
+ expect(tokens[0].value).toBe('~')
189
+ expect(tokens[1].value).toBe('~5')
190
+ expect(tokens[2].value).toBe('~-3')
191
+ expect(tokens[3].value).toBe('^')
192
+ expect(tokens[4].value).toBe('^10')
193
+ expect(tokens[5].value).toBe('^-2')
168
194
  })
169
195
 
170
196
  it('tokenizes comparison operators', () => {
@@ -191,6 +217,11 @@ describe('Lexer', () => {
191
217
  const tokens = tokenize('=>')
192
218
  expect(kinds(tokens)).toEqual(['=>', 'eof'])
193
219
  })
220
+
221
+ it('tokenizes static method separators for impl methods', () => {
222
+ const tokens = tokenize('Point::new()')
223
+ expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof'])
224
+ })
194
225
  })
195
226
 
196
227
  describe('delimiters', () => {