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
@@ -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 = `
@@ -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', () => {
@@ -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', () => {
@@ -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', () => {
@@ -72,6 +78,13 @@ describe('Lexer', () => {
72
78
  ['eof', ''],
73
79
  ]);
74
80
  });
81
+ it('tokenizes f-strings as a dedicated token', () => {
82
+ const tokens = tokenize('f"Hello {name}!"');
83
+ expect(tokens.map(t => [t.kind, t.value])).toEqual([
84
+ ['f_string', 'Hello {name}!'],
85
+ ['eof', ''],
86
+ ]);
87
+ });
75
88
  it('tokenizes byte literals (b suffix)', () => {
76
89
  const tokens = tokenize('20b 0B 127b');
77
90
  expect(tokens.map(t => [t.kind, t.value])).toEqual([
@@ -146,8 +159,18 @@ describe('Lexer', () => {
146
159
  });
147
160
  describe('operators', () => {
148
161
  it('tokenizes arithmetic operators', () => {
149
- const tokens = tokenize('+ - * / % ~ ^');
150
- expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', '~', '^', 'eof']);
162
+ const tokens = tokenize('+ - * / %');
163
+ expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', 'eof']);
164
+ });
165
+ it('tokenizes relative and local coordinates', () => {
166
+ const tokens = tokenize('~ ~5 ~-3 ^ ^10 ^-2');
167
+ expect(kinds(tokens)).toEqual(['rel_coord', 'rel_coord', 'rel_coord', 'local_coord', 'local_coord', 'local_coord', 'eof']);
168
+ expect(tokens[0].value).toBe('~');
169
+ expect(tokens[1].value).toBe('~5');
170
+ expect(tokens[2].value).toBe('~-3');
171
+ expect(tokens[3].value).toBe('^');
172
+ expect(tokens[4].value).toBe('^10');
173
+ expect(tokens[5].value).toBe('^-2');
151
174
  });
152
175
  it('tokenizes comparison operators', () => {
153
176
  const tokens = tokenize('== != < <= > >=');
@@ -169,6 +192,10 @@ describe('Lexer', () => {
169
192
  const tokens = tokenize('=>');
170
193
  expect(kinds(tokens)).toEqual(['=>', 'eof']);
171
194
  });
195
+ it('tokenizes static method separators for impl methods', () => {
196
+ const tokens = tokenize('Point::new()');
197
+ expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof']);
198
+ });
172
199
  });
173
200
  describe('delimiters', () => {
174
201
  it('tokenizes all delimiters', () => {
@@ -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', () => {
@@ -421,6 +498,14 @@ fn choose(dir: Direction) {
421
498
  const rawCmds = getRawCommands(fn);
422
499
  expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$score","objective":"rs"}},{"text":" points"}]');
423
500
  });
501
+ it('lowers f-string output builtins to tellraw/title JSON components', () => {
502
+ const ir = compile('fn test() { let score: int = 7; say(f"Score: {score}"); tellraw(@a, f"Score: {score}"); actionbar(@s, f"Score: {score}"); title(@s, f"Score: {score}"); }');
503
+ const fn = getFunction(ir, 'test');
504
+ const rawCmds = getRawCommands(fn);
505
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]');
506
+ expect(rawCmds).toContain('title @s actionbar ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]');
507
+ expect(rawCmds).toContain('title @s title ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]');
508
+ });
424
509
  it('lowers summon()', () => {
425
510
  const ir = compile('fn test() { summon("zombie"); }');
426
511
  const fn = getFunction(ir, 'test');
@@ -515,12 +600,12 @@ fn test() {
515
600
  `);
516
601
  const fn = getFunction(ir, 'test');
517
602
  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');
603
+ expect(rawCmds).toContain('scoreboard objectives setdisplay sidebar test.kills');
604
+ expect(rawCmds).toContain('scoreboard objectives setdisplay list test.coins');
605
+ expect(rawCmds).toContain('scoreboard objectives setdisplay belowName test.hp');
521
606
  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');
607
+ expect(rawCmds).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"');
608
+ expect(rawCmds).toContain('scoreboard objectives remove test.kills');
524
609
  });
525
610
  it('lowers bossbar management builtins', () => {
526
611
  const ir = compile(`
@@ -588,6 +673,40 @@ fn test() {
588
673
  const rawCmds = getRawCommands(fn);
589
674
  expect(rawCmds).toContain('random reset loot 42');
590
675
  });
676
+ it('lowers setTimeout() to a scheduled helper function', () => {
677
+ const ir = compile('fn test() { setTimeout(100, () => { say("hi"); }); }');
678
+ const fn = getFunction(ir, 'test');
679
+ const timeoutFn = getFunction(ir, '__timeout_0');
680
+ const rawCmds = getRawCommands(fn);
681
+ const timeoutCmds = getRawCommands(timeoutFn);
682
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t');
683
+ expect(timeoutCmds).toContain('say hi');
684
+ });
685
+ it('lowers setInterval() to a self-rescheduling helper function', () => {
686
+ const ir = compile('fn test() { setInterval(20, () => { say("tick"); }); }');
687
+ const fn = getFunction(ir, 'test');
688
+ const intervalFn = getFunction(ir, '__interval_0');
689
+ const intervalBodyFn = getFunction(ir, '__interval_body_0');
690
+ const rawCmds = getRawCommands(fn);
691
+ const intervalCmds = getRawCommands(intervalFn);
692
+ const intervalBodyCmds = getRawCommands(intervalBodyFn);
693
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
694
+ expect(intervalCmds).toContain('function test:__interval_body_0');
695
+ expect(intervalCmds).toContain('schedule function test:__interval_0 20t');
696
+ expect(intervalBodyCmds).toContain('say tick');
697
+ });
698
+ it('lowers clearInterval() to schedule clear for the generated interval function', () => {
699
+ const ir = compile(`
700
+ fn test() {
701
+ let intervalId: int = setInterval(20, () => { say("tick"); });
702
+ clearInterval(intervalId);
703
+ }
704
+ `);
705
+ const fn = getFunction(ir, 'test');
706
+ const rawCmds = getRawCommands(fn);
707
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
708
+ expect(rawCmds).toContain('schedule clear test:__interval_0');
709
+ });
591
710
  it('lowers data_get from entity', () => {
592
711
  const ir = compile('fn test() { let item_count: int = data_get("entity", "@s", "SelectedItem.Count"); }');
593
712
  const fn = getFunction(ir, 'test');
@@ -628,19 +747,25 @@ fn test() {
628
747
  const ir = compile('fn test() { let score: int = scoreboard_get(@s, "score"); }');
629
748
  const fn = getFunction(ir, 'test');
630
749
  const rawCmds = getRawCommands(fn);
631
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s score'))).toBe(true);
750
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s test.score'))).toBe(true);
632
751
  });
633
752
  it('accepts bare selector targets in scoreboard_set', () => {
634
753
  const ir = compile('fn test() { scoreboard_set(@a, "kills", 0); }');
635
754
  const fn = getFunction(ir, 'test');
636
755
  const rawCmds = getRawCommands(fn);
637
- expect(rawCmds).toContain('scoreboard players set @a kills 0');
756
+ expect(rawCmds).toContain('scoreboard players set @a test.kills 0');
757
+ });
758
+ it('skips prefixing raw mc_name objectives', () => {
759
+ const ir = compile('fn test() { scoreboard_set(@s, #health, 100); }');
760
+ const fn = getFunction(ir, 'test');
761
+ const rawCmds = getRawCommands(fn);
762
+ expect(rawCmds).toContain('scoreboard players set @s health 100');
638
763
  });
639
764
  it('warns on quoted selectors in scoreboard_get', () => {
640
765
  const { ir, warnings } = compileWithWarnings('fn test() { let score: int = scoreboard_get("@s", "score"); }');
641
766
  const fn = getFunction(ir, 'test');
642
767
  const rawCmds = getRawCommands(fn);
643
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s score'))).toBe(true);
768
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get @s test.score'))).toBe(true);
644
769
  expect(warnings).toContainEqual(expect.objectContaining({
645
770
  code: 'W_QUOTED_SELECTOR',
646
771
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
@@ -650,7 +775,7 @@ fn test() {
650
775
  const { ir, warnings } = compileWithWarnings('fn test() { let total: int = scoreboard_get("#global", "total"); }');
651
776
  const fn = getFunction(ir, 'test');
652
777
  const rawCmds = getRawCommands(fn);
653
- expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get #global total'))).toBe(true);
778
+ expect(rawCmds.some(cmd => cmd.includes('run scoreboard players get #global test.total'))).toBe(true);
654
779
  expect(warnings).toHaveLength(0);
655
780
  });
656
781
  it('warns on quoted selectors in data_get entity targets', () => {
@@ -663,6 +788,37 @@ fn test() {
663
788
  message: 'Quoted selector "@s" is deprecated; pass @s without quotes',
664
789
  }));
665
790
  });
791
+ it('keeps already-qualified scoreboard objectives unchanged', () => {
792
+ const ir = compile('fn test() { scoreboard_set(@s, "custom.timer", 5); }');
793
+ const fn = getFunction(ir, 'test');
794
+ const rawCmds = getRawCommands(fn);
795
+ expect(rawCmds).toContain('scoreboard players set @s custom.timer 5');
796
+ });
797
+ });
798
+ describe('timer builtins', () => {
799
+ it('lowers timer builtins into schedule commands and wrapper functions', () => {
800
+ const ir = compile(`
801
+ fn test() {
802
+ let intervalId: int = setInterval(20, () => {
803
+ say("tick");
804
+ });
805
+ setTimeout(100, () => {
806
+ say("later");
807
+ });
808
+ clearInterval(intervalId);
809
+ }
810
+ `);
811
+ const fn = getFunction(ir, 'test');
812
+ const rawCmds = getRawCommands(fn);
813
+ expect(rawCmds).toContain('schedule function test:__interval_0 20t');
814
+ expect(rawCmds).toContain('schedule function test:__timeout_0 100t');
815
+ expect(rawCmds).toContain('schedule clear test:__interval_0');
816
+ const intervalFn = getFunction(ir, '__interval_0');
817
+ expect(getRawCommands(intervalFn)).toEqual([
818
+ 'function test:__interval_body_0',
819
+ 'schedule function test:__interval_0 20t',
820
+ ]);
821
+ });
666
822
  });
667
823
  describe('decorators', () => {
668
824
  it('marks @tick function', () => {
@@ -681,6 +837,13 @@ fn test() {
681
837
  const fn = getFunction(ir, 'handle_advancement');
682
838
  expect(fn.eventTrigger).toEqual({ kind: 'advancement', value: 'story/mine_diamond' });
683
839
  });
840
+ it('marks @on event functions and binds player to @s', () => {
841
+ const ir = compile('@on(PlayerDeath) fn handle_death(player: Player) { tp(player, @p); }');
842
+ const fn = getFunction(ir, 'handle_death');
843
+ expect(fn.eventHandler).toEqual({ eventType: 'PlayerDeath', tag: 'rs.just_died' });
844
+ expect(fn.params).toEqual([]);
845
+ expect(getRawCommands(fn)).toContain('tp @s @p');
846
+ });
684
847
  });
685
848
  describe('selectors', () => {
686
849
  it('converts selector with filters to string', () => {