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
@@ -52,6 +52,7 @@ const MC_HOST = process.env.MC_HOST ?? 'localhost';
52
52
  const MC_PORT = parseInt(process.env.MC_PORT ?? '25561');
53
53
  const MC_SERVER_DIR = process.env.MC_SERVER_DIR ?? path.join(process.env.HOME, 'mc-test-server');
54
54
  const DATAPACK_DIR = path.join(MC_SERVER_DIR, 'world', 'datapacks', 'redscript-test');
55
+ const FIXTURE_DIR = path.join(__dirname, 'fixtures');
55
56
  let serverOnline = false;
56
57
  let mc;
57
58
  /** Write compiled RedScript source into the shared test datapack directory.
@@ -84,9 +85,22 @@ function writeFixture(source, namespace) {
84
85
  }
85
86
  }
86
87
  }
88
+ function writeFixtureFile(fileName, namespace) {
89
+ writeFixture(fs.readFileSync(path.join(FIXTURE_DIR, fileName), 'utf-8'), namespace);
90
+ }
91
+ async function waitForServer(client, timeoutMs = 30000) {
92
+ const deadline = Date.now() + timeoutMs;
93
+ while (Date.now() < deadline) {
94
+ if (await client.isOnline()) {
95
+ return true;
96
+ }
97
+ await new Promise(resolve => setTimeout(resolve, 1000));
98
+ }
99
+ return false;
100
+ }
87
101
  beforeAll(async () => {
88
102
  mc = new client_1.MCTestClient(MC_HOST, MC_PORT);
89
- serverOnline = await mc.isOnline();
103
+ serverOnline = await waitForServer(mc);
90
104
  if (!serverOnline) {
91
105
  console.warn(`⚠ MC server not running at ${MC_HOST}:${MC_PORT} — skipping integration tests`);
92
106
  console.warn(` Run: MC_SERVER_DIR=~/mc-test-server npx ts-node src/mc-test/setup.ts`);
@@ -104,16 +118,16 @@ beforeAll(async () => {
104
118
  writeFixture(`
105
119
  @tick
106
120
  fn on_tick() {
107
- scoreboard_set("#tick_counter", "ticks", scoreboard_get("#tick_counter", "ticks") + 1);
121
+ scoreboard_set("#tick_counter", #ticks, scoreboard_get("#tick_counter", #ticks) + 1);
108
122
  }
109
123
  `, 'tick_test');
110
124
  writeFixture(`
111
125
  fn check_score() {
112
- let x: int = scoreboard_get("#check_x", "test_score");
126
+ let x: int = scoreboard_get("#check_x", #test_score);
113
127
  if (x > 5) {
114
- scoreboard_set("#check_x", "result", 1);
128
+ scoreboard_set("#check_x", #result, 1);
115
129
  } else {
116
- scoreboard_set("#check_x", "result", 0);
130
+ scoreboard_set("#check_x", #result, 0);
117
131
  }
118
132
  }
119
133
  `, 'inline_test');
@@ -122,30 +136,30 @@ beforeAll(async () => {
122
136
  writeFixture(`
123
137
  @tick
124
138
  fn game_tick() {
125
- let time: int = scoreboard_get("#game", "timer");
139
+ let time: int = scoreboard_get("#game", #timer);
126
140
  if (time > 0) {
127
- scoreboard_set("#game", "timer", time - 1);
141
+ scoreboard_set("#game", #timer, time - 1);
128
142
  }
129
143
  if (time == 1) {
130
- scoreboard_set("#game", "ended", 1);
144
+ scoreboard_set("#game", #ended, 1);
131
145
  }
132
146
  }
133
147
  fn start_game() {
134
- scoreboard_set("#game", "timer", 5);
135
- scoreboard_set("#game", "ended", 0);
148
+ scoreboard_set("#game", #timer, 5);
149
+ scoreboard_set("#game", #ended, 0);
136
150
  }
137
151
  `, 'game_loop');
138
152
  // Scenario B: two functions, same temp var namespace — verify no collision
139
153
  writeFixture(`
140
154
  fn calc_sum() {
141
- let a: int = scoreboard_get("#math", "val_a");
142
- let b: int = scoreboard_get("#math", "val_b");
143
- scoreboard_set("#math", "sum", a + b);
155
+ let a: int = scoreboard_get("#math", #val_a);
156
+ let b: int = scoreboard_get("#math", #val_b);
157
+ scoreboard_set("#math", #sum, a + b);
144
158
  }
145
159
  fn calc_product() {
146
- let x: int = scoreboard_get("#math", "val_x");
147
- let y: int = scoreboard_get("#math", "val_y");
148
- scoreboard_set("#math", "product", x * y);
160
+ let x: int = scoreboard_get("#math", #val_x);
161
+ let y: int = scoreboard_get("#math", #val_y);
162
+ scoreboard_set("#math", #product, x * y);
149
163
  }
150
164
  fn run_both() {
151
165
  calc_sum();
@@ -155,16 +169,16 @@ beforeAll(async () => {
155
169
  // Scenario C: 3-deep call chain, each step modifies shared state
156
170
  writeFixture(`
157
171
  fn step3() {
158
- let v: int = scoreboard_get("#chain", "val");
159
- scoreboard_set("#chain", "val", v * 2);
172
+ let v: int = scoreboard_get("#chain", #val);
173
+ scoreboard_set("#chain", #val, v * 2);
160
174
  }
161
175
  fn step2() {
162
- let v: int = scoreboard_get("#chain", "val");
163
- scoreboard_set("#chain", "val", v + 5);
176
+ let v: int = scoreboard_get("#chain", #val);
177
+ scoreboard_set("#chain", #val, v + 5);
164
178
  step3();
165
179
  }
166
180
  fn step1() {
167
- scoreboard_set("#chain", "val", 10);
181
+ scoreboard_set("#chain", #val, 10);
168
182
  step2();
169
183
  }
170
184
  `, 'call_chain');
@@ -180,10 +194,10 @@ beforeAll(async () => {
180
194
  // Scenario E: for-range loop — loop counter increments exactly N times
181
195
  writeFixture(`
182
196
  fn count_to_five() {
183
- scoreboard_set("#range", "counter", 0);
197
+ scoreboard_set("#range", #counter, 0);
184
198
  for i in 0..5 {
185
- let c: int = scoreboard_get("#range", "counter");
186
- scoreboard_set("#range", "counter", c + 1);
199
+ let c: int = scoreboard_get("#range", #counter);
200
+ scoreboard_set("#range", #counter, c + 1);
187
201
  }
188
202
  }
189
203
  `, 'range_test');
@@ -194,48 +208,48 @@ beforeAll(async () => {
194
208
  }
195
209
  fn run_nested() {
196
210
  let a: int = triple(4);
197
- scoreboard_set("#nested", "result", a);
211
+ scoreboard_set("#nested", #result, a);
198
212
  }
199
213
  `, 'nested_test');
200
214
  // Scenario G: match statement dispatches to correct branch
201
215
  writeFixture(`
202
216
  fn classify(x: int) {
203
217
  match (x) {
204
- 1 => { scoreboard_set("#match", "out", 10); }
205
- 2 => { scoreboard_set("#match", "out", 20); }
206
- 3 => { scoreboard_set("#match", "out", 30); }
207
- _ => { scoreboard_set("#match", "out", -1); }
218
+ 1 => { scoreboard_set("#match", #out, 10); }
219
+ 2 => { scoreboard_set("#match", #out, 20); }
220
+ 3 => { scoreboard_set("#match", #out, 30); }
221
+ _ => { scoreboard_set("#match", #out, -1); }
208
222
  }
209
223
  }
210
224
  `, 'match_test');
211
225
  // Scenario H: while loop counts down
212
226
  writeFixture(`
213
227
  fn countdown() {
214
- scoreboard_set("#wloop", "i", 10);
215
- scoreboard_set("#wloop", "steps", 0);
216
- let i: int = scoreboard_get("#wloop", "i");
228
+ scoreboard_set("#wloop", #i, 10);
229
+ scoreboard_set("#wloop", #steps, 0);
230
+ let i: int = scoreboard_get("#wloop", #i);
217
231
  while (i > 0) {
218
- let s: int = scoreboard_get("#wloop", "steps");
219
- scoreboard_set("#wloop", "steps", s + 1);
232
+ let s: int = scoreboard_get("#wloop", #steps);
233
+ scoreboard_set("#wloop", #steps, s + 1);
220
234
  i = i - 1;
221
- scoreboard_set("#wloop", "i", i);
235
+ scoreboard_set("#wloop", #i, i);
222
236
  }
223
237
  }
224
238
  `, 'while_test');
225
239
  // Scenario I: multiple if/else branches (boundary test)
226
240
  writeFixture(`
227
241
  fn classify_score() {
228
- let x: int = scoreboard_get("#boundary", "input");
242
+ let x: int = scoreboard_get("#boundary", #input);
229
243
  if (x > 100) {
230
- scoreboard_set("#boundary", "tier", 3);
244
+ scoreboard_set("#boundary", #tier, 3);
231
245
  } else {
232
246
  if (x > 50) {
233
- scoreboard_set("#boundary", "tier", 2);
247
+ scoreboard_set("#boundary", #tier, 2);
234
248
  } else {
235
249
  if (x > 0) {
236
- scoreboard_set("#boundary", "tier", 1);
250
+ scoreboard_set("#boundary", #tier, 1);
237
251
  } else {
238
- scoreboard_set("#boundary", "tier", 0);
252
+ scoreboard_set("#boundary", #tier, 0);
239
253
  }
240
254
  }
241
255
  }
@@ -255,31 +269,37 @@ beforeAll(async () => {
255
269
  let a: int = 2;
256
270
  let b: int = 3;
257
271
  let c: int = 4;
258
- scoreboard_set("#order", "r1", a + b * c);
259
- scoreboard_set("#order", "r2", (a + b) * c);
272
+ scoreboard_set("#order", #r1, a + b * c);
273
+ scoreboard_set("#order", #r2, (a + b) * c);
260
274
  let d: int = 100;
261
275
  let e: int = d / 3;
262
- scoreboard_set("#order", "r3", e);
276
+ scoreboard_set("#order", #r3, e);
263
277
  }
264
278
  `, 'order_test');
265
279
  // Scenario L: scoreboard read-modify-write chain
266
280
  writeFixture(`
267
281
  fn chain_rmw() {
268
- scoreboard_set("#rmw", "v", 1);
269
- let v: int = scoreboard_get("#rmw", "v");
270
- scoreboard_set("#rmw", "v", v * 2);
271
- v = scoreboard_get("#rmw", "v");
272
- scoreboard_set("#rmw", "v", v * 2);
273
- v = scoreboard_get("#rmw", "v");
274
- scoreboard_set("#rmw", "v", v * 2);
282
+ scoreboard_set("#rmw", #v, 1);
283
+ let v: int = scoreboard_get("#rmw", #v);
284
+ scoreboard_set("#rmw", #v, v * 2);
285
+ v = scoreboard_get("#rmw", #v);
286
+ scoreboard_set("#rmw", #v, v * 2);
287
+ v = scoreboard_get("#rmw", #v);
288
+ scoreboard_set("#rmw", #v, v * 2);
275
289
  }
276
290
  `, 'rmw_test');
291
+ writeFixtureFile('impl-test.mcrs', 'impl_test');
292
+ writeFixtureFile('timeout-test.mcrs', 'timeout_test');
293
+ writeFixtureFile('interval-test.mcrs', 'interval_test');
294
+ writeFixtureFile('is-check-test.mcrs', 'is_check_test');
295
+ writeFixtureFile('event-test.mcrs', 'event_test');
277
296
  // ── Full reset + safe data reload ────────────────────────────────────
278
297
  await mc.fullReset();
279
298
  // Pre-create scoreboards
280
299
  for (const obj of ['ticks', 'seconds', 'test_score', 'result', 'calc', 'rs',
281
300
  'timer', 'ended', 'val_a', 'val_b', 'sum', 'val_x', 'val_y', 'product', 'val',
282
- 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v']) {
301
+ 'counter', 'out', 'i', 'steps', 'input', 'tier', 'r1', 'r2', 'r3', 'v',
302
+ 'done', 'fired', 'players', 'zombies']) {
283
303
  await mc.command(`/scoreboard objectives add ${obj} dummy`).catch(() => { });
284
304
  }
285
305
  await mc.command('/scoreboard players set counter ticks 0');
@@ -637,4 +657,78 @@ describe('E2E Scenario Tests', () => {
637
657
  console.log(` RMW chain: 1→2→4→8, got ${v} (expect 8) ✓`);
638
658
  });
639
659
  });
660
+ describe('MC Integration - New Features', () => {
661
+ test('impl-test.mcrs: Timer::new/start/tick/done works in-game', async () => {
662
+ if (!serverOnline)
663
+ return;
664
+ await mc.command('/scoreboard players set #impl done 0');
665
+ await mc.command('/scoreboard players set timer_ticks rs 0');
666
+ await mc.command('/scoreboard players set timer_active rs 0');
667
+ await mc.command('/function impl_test:__load').catch(() => { });
668
+ await mc.command('/function impl_test:test');
669
+ await mc.ticks(5);
670
+ const done = await mc.scoreboard('#impl', 'done');
671
+ const ticks = await mc.scoreboard('timer_ticks', 'rs');
672
+ expect(done).toBe(1);
673
+ expect(ticks).toBe(3);
674
+ });
675
+ test('timeout-test.mcrs: setTimeout executes after delay', async () => {
676
+ if (!serverOnline)
677
+ return;
678
+ await mc.command('/scoreboard players set #timeout fired 0');
679
+ await mc.command('/function timeout_test:__load').catch(() => { });
680
+ await mc.command('/function timeout_test:start');
681
+ await mc.ticks(10);
682
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(0);
683
+ await mc.ticks(15);
684
+ expect(await mc.scoreboard('#timeout', 'fired')).toBe(1);
685
+ });
686
+ test('interval-test.mcrs: setInterval repeats on schedule', async () => {
687
+ if (!serverOnline)
688
+ return;
689
+ await mc.command('/scoreboard players set #interval ticks 0');
690
+ await mc.command('/function interval_test:__load').catch(() => { });
691
+ await mc.command('/function interval_test:start');
692
+ await mc.ticks(70);
693
+ const count = await mc.scoreboard('#interval', 'ticks');
694
+ expect(count).toBeGreaterThanOrEqual(3);
695
+ expect(count).toBeLessThanOrEqual(3);
696
+ });
697
+ test('is-check-test.mcrs: foreach is-narrowing only matches zombie entities', async () => {
698
+ if (!serverOnline)
699
+ return;
700
+ await mc.fullReset({ clearArea: false, killEntities: true, resetScoreboards: false });
701
+ await mc.command('/scoreboard players set #is_check players 0');
702
+ await mc.command('/scoreboard players set #is_check zombies 0');
703
+ await mc.command('/function is_check_test:__load').catch(() => { });
704
+ await mc.command('/summon minecraft:zombie 0 65 0');
705
+ await mc.command('/tag @e[type=minecraft:zombie,sort=nearest,limit=1] add is_check_target');
706
+ await mc.command('/summon minecraft:armor_stand 2 65 0');
707
+ await mc.command('/tag @e[type=minecraft:armor_stand,sort=nearest,limit=1] add is_check_target');
708
+ await mc.command('/function is_check_test:check_types');
709
+ await mc.ticks(5);
710
+ const zombies = await mc.scoreboard('#is_check', 'zombies');
711
+ const players = await mc.scoreboard('#is_check', 'players');
712
+ const zombieEntities = await mc.entities('@e[type=minecraft:zombie,tag=is_check_target]');
713
+ const standEntities = await mc.entities('@e[type=minecraft:armor_stand,tag=is_check_target]');
714
+ expect(zombies).toBe(1);
715
+ expect(players).toBe(0);
716
+ expect(zombieEntities).toHaveLength(0);
717
+ expect(standEntities).toHaveLength(1);
718
+ await mc.command('/kill @e[tag=is_check_target]').catch(() => { });
719
+ });
720
+ test('event-test.mcrs: @on(PlayerDeath) compiles and loads', async () => {
721
+ if (!serverOnline)
722
+ return;
723
+ // Verify the event system compiles correctly
724
+ await mc.command('/function event_test:__load').catch(() => { });
725
+ await mc.ticks(5);
726
+ // Verify the trigger function exists
727
+ const result = await mc.command('/function event_test:trigger_fake_death');
728
+ expect(result.ok).toBe(true);
729
+ // Verify __tick exists (event dispatcher)
730
+ const tickResult = await mc.command('/function event_test:__tick').catch(() => ({ ok: false }));
731
+ expect(tickResult.ok).toBe(true);
732
+ });
733
+ });
640
734
  //# sourceMappingURL=mc-integration.test.js.map
@@ -80,6 +80,18 @@ fn chat() {
80
80
  `, 'interpolation');
81
81
  expect(errors).toHaveLength(0);
82
82
  });
83
+ test('f-strings generate valid tellraw/title commands', () => {
84
+ const errors = validateSource(validator, `
85
+ fn chat() {
86
+ let score: int = 7;
87
+ say(f"You have {score} points");
88
+ tellraw(@a, f"Score: {score}");
89
+ actionbar(@s, f"Score: {score}");
90
+ title(@s, f"Score: {score}");
91
+ }
92
+ `, 'f-string');
93
+ expect(errors).toHaveLength(0);
94
+ });
83
95
  test('array operations generate valid data commands', () => {
84
96
  const errors = validateSource(validator, `
85
97
  fn arrays() {
@@ -26,11 +26,11 @@ fn turret_tick() {
26
26
  const result = (0, index_1.compile)(source, { namespace: 'test' });
27
27
  const parent = getFileContent(result.files, 'data/test/function/turret_tick.mcfunction');
28
28
  const loopBody = getFileContent(result.files, 'data/test/function/turret_tick/foreach_0.mcfunction');
29
- const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config turret_range';
29
+ const hoistedRead = 'execute store result score $_0 rs run scoreboard players get config test.turret_range';
30
30
  const executeCall = 'execute as @e[tag=turret] run function test:turret_tick/foreach_0';
31
31
  expect(parent).toContain(hoistedRead);
32
32
  expect(parent.indexOf(hoistedRead)).toBeLessThan(parent.indexOf(executeCall));
33
- expect(loopBody).not.toContain('scoreboard players get config turret_range');
33
+ expect(loopBody).not.toContain('scoreboard players get config test.turret_range');
34
34
  });
35
35
  });
36
36
  describe('CSE', () => {
@@ -46,7 +46,7 @@ fn read_twice() {
46
46
  `;
47
47
  const result = (0, index_1.compile)(source, { namespace: 'test' });
48
48
  const fn = getFileContent(result.files, 'data/test/function/read_twice.mcfunction');
49
- const readMatches = fn.match(/scoreboard players get @s coins/g) ?? [];
49
+ const readMatches = fn.match(/scoreboard players get @s test\.coins/g) ?? [];
50
50
  expect(readMatches).toHaveLength(1);
51
51
  expect(fn).toContain('scoreboard players operation $_1 rs = $_0 rs');
52
52
  });
@@ -23,6 +23,7 @@ describe('Parser', () => {
23
23
  const program = parse('');
24
24
  expect(program.namespace).toBe('test');
25
25
  expect(program.declarations).toEqual([]);
26
+ expect(program.implBlocks).toEqual([]);
26
27
  expect(program.enums).toEqual([]);
27
28
  expect(program.consts).toEqual([]);
28
29
  });
@@ -90,6 +91,12 @@ describe('Parser', () => {
90
91
  { name: 'on_death' },
91
92
  ]);
92
93
  });
94
+ it('parses @on event decorators', () => {
95
+ const program = parse('@on(PlayerDeath)\nfn handle_death(player: Player) {}');
96
+ expect(program.declarations[0].decorators).toEqual([
97
+ { name: 'on', args: { eventType: 'PlayerDeath' } },
98
+ ]);
99
+ });
93
100
  });
94
101
  describe('types', () => {
95
102
  it('parses primitive types', () => {
@@ -134,6 +141,50 @@ describe('Parser', () => {
134
141
  },
135
142
  ]);
136
143
  });
144
+ it('parses impl blocks', () => {
145
+ const program = parse(`
146
+ struct Timer { duration: int }
147
+
148
+ impl Timer {
149
+ fn new(duration: int): Timer {
150
+ return { duration: duration };
151
+ }
152
+
153
+ fn start(self) {}
154
+ }
155
+ `);
156
+ expect(program.implBlocks).toHaveLength(1);
157
+ expect(program.implBlocks[0].typeName).toBe('Timer');
158
+ expect(program.implBlocks[0].methods.map(method => method.name)).toEqual(['new', 'start']);
159
+ expect(program.implBlocks[0].methods[1].params[0]).toEqual({
160
+ name: 'self',
161
+ type: { kind: 'struct', name: 'Timer' },
162
+ default: undefined,
163
+ });
164
+ });
165
+ it('parses impl blocks with static and instance methods', () => {
166
+ const program = parse(`
167
+ struct Point { x: int, y: int }
168
+
169
+ impl Point {
170
+ fn new(x: int, y: int) -> Point {
171
+ return { x: x, y: y };
172
+ }
173
+
174
+ fn distance(self) -> int {
175
+ return self.x + self.y;
176
+ }
177
+ }
178
+ `);
179
+ expect(program.implBlocks).toHaveLength(1);
180
+ expect(program.implBlocks[0].typeName).toBe('Point');
181
+ expect(program.implBlocks[0].methods[0].params.map(param => param.name)).toEqual(['x', 'y']);
182
+ expect(program.implBlocks[0].methods[1].params[0]).toEqual({
183
+ name: 'self',
184
+ type: { kind: 'struct', name: 'Point' },
185
+ default: undefined,
186
+ });
187
+ });
137
188
  });
138
189
  describe('statements', () => {
139
190
  it('parses let statement', () => {
@@ -173,6 +224,26 @@ describe('Parser', () => {
173
224
  expect(stmt.kind).toBe('if');
174
225
  expect(stmt.else_).toHaveLength(1);
175
226
  });
227
+ it('parses entity is-checks in if conditions', () => {
228
+ const stmt = parseStmt('if (e is Player) { kill(@s); }');
229
+ expect(stmt.kind).toBe('if');
230
+ expect(stmt.cond).toEqual({
231
+ kind: 'is_check',
232
+ expr: { kind: 'ident', name: 'e' },
233
+ entityType: 'Player',
234
+ });
235
+ });
236
+ it('parses entity is-checks inside foreach bodies', () => {
237
+ const stmt = parseStmt('foreach (e in @e) { if (e is Zombie) { kill(e); } }');
238
+ expect(stmt.kind).toBe('foreach');
239
+ const innerIf = stmt.body[0];
240
+ expect(innerIf.kind).toBe('if');
241
+ expect(innerIf.cond).toEqual({
242
+ kind: 'is_check',
243
+ expr: { kind: 'ident', name: 'e' },
244
+ entityType: 'Zombie',
245
+ });
246
+ });
176
247
  it('parses while statement', () => {
177
248
  const stmt = parseStmt('while (i > 0) { i = i - 1; }');
178
249
  expect(stmt.kind).toBe('while');
@@ -358,6 +429,16 @@ describe('Parser', () => {
358
429
  ],
359
430
  });
360
431
  });
432
+ it('parses f-string literal', () => {
433
+ const expr = parseExpr('f"Score: {x}"');
434
+ expect(expr).toEqual({
435
+ kind: 'f_string',
436
+ parts: [
437
+ { kind: 'text', value: 'Score: ' },
438
+ { kind: 'expr', expr: { kind: 'ident', name: 'x' } },
439
+ ],
440
+ });
441
+ });
361
442
  it('parses boolean literals', () => {
362
443
  expect(parseExpr('true')).toEqual({ kind: 'bool_lit', value: true });
363
444
  expect(parseExpr('false')).toEqual({ kind: 'bool_lit', value: false });
@@ -429,6 +510,15 @@ describe('Parser', () => {
429
510
  });
430
511
  });
431
512
  });
513
+ it('parses static method calls', () => {
514
+ const expr = parseExpr('Timer::new(100)');
515
+ expect(expr).toEqual({
516
+ kind: 'static_call',
517
+ type: 'Timer',
518
+ method: 'new',
519
+ args: [{ kind: 'int_lit', value: 100 }],
520
+ });
521
+ });
432
522
  describe('binary operators', () => {
433
523
  it('parses arithmetic', () => {
434
524
  const expr = parseExpr('1 + 2');
@@ -81,7 +81,7 @@ fn compute() {
81
81
  `);
82
82
  runtime.load();
83
83
  runtime.execFunction('compute');
84
- expect(runtime.getScore('math', 'result')).toBe(11);
84
+ expect(runtime.getScore('math', 'runtime.result')).toBe(11);
85
85
  });
86
86
  it('captures say, announce, actionbar, and title output in the chat log', () => {
87
87
  const runtime = loadCompiledProgram(`
@@ -107,6 +107,19 @@ fn chat() {
107
107
  let score: int = 7;
108
108
  say("You have \${score} points");
109
109
  }
110
+ `);
111
+ runtime.load();
112
+ runtime.execFunction('chat');
113
+ expect(runtime.getChatLog()).toEqual([
114
+ 'You have 7 points',
115
+ ]);
116
+ });
117
+ it('renders f-strings through tellraw score components', () => {
118
+ const runtime = loadCompiledProgram(`
119
+ fn chat() {
120
+ let score: int = 7;
121
+ say(f"You have {score} points");
122
+ }
110
123
  `);
111
124
  runtime.load();
112
125
  runtime.execFunction('chat');
@@ -146,8 +159,8 @@ fn arrays() {
146
159
  `);
147
160
  runtime.load();
148
161
  runtime.execFunction('arrays');
149
- expect(runtime.getScore('arrays', 'len')).toBe(1);
150
- expect(runtime.getScore('arrays', 'last')).toBe(9);
162
+ expect(runtime.getScore('arrays', 'runtime.len')).toBe(1);
163
+ expect(runtime.getScore('arrays', 'runtime.last')).toBe(9);
151
164
  expect(runtime.getStorage('rs:heap.arr')).toEqual([4]);
152
165
  });
153
166
  it('tracks world state, weather, and time from compiled world commands', () => {
@@ -182,7 +195,7 @@ fn pulse() {
182
195
  `);
183
196
  runtime.load();
184
197
  runtime.ticks(10);
185
- expect(runtime.getScore('pulse', 'count')).toBe(2);
198
+ expect(runtime.getScore('pulse', 'runtime.count')).toBe(2);
186
199
  });
187
200
  it('executes only the matching match arm', () => {
188
201
  const runtime = loadCompiledProgram(`
@@ -229,7 +242,7 @@ fn test() {
229
242
  `);
230
243
  runtime.load();
231
244
  runtime.execFunction('test');
232
- expect(runtime.getScore('lambda', 'direct')).toBe(10);
245
+ expect(runtime.getScore('lambda', 'runtime.direct')).toBe(10);
233
246
  });
234
247
  it('executes lambdas passed as callback arguments', () => {
235
248
  const runtime = loadCompiledProgram(`
@@ -244,7 +257,7 @@ fn test() {
244
257
  `);
245
258
  runtime.load();
246
259
  runtime.execFunction('test');
247
- expect(runtime.getScore('lambda', 'callback')).toBe(15);
260
+ expect(runtime.getScore('lambda', 'runtime.callback')).toBe(15);
248
261
  });
249
262
  it('executes block-body lambdas', () => {
250
263
  const runtime = loadCompiledProgram(`
@@ -259,7 +272,7 @@ fn test() {
259
272
  `);
260
273
  runtime.load();
261
274
  runtime.execFunction('test');
262
- expect(runtime.getScore('lambda', 'block')).toBe(11);
275
+ expect(runtime.getScore('lambda', 'runtime.block')).toBe(11);
263
276
  });
264
277
  it('executes immediately-invoked expression-body lambdas', () => {
265
278
  const runtime = loadCompiledProgram(`
@@ -270,7 +283,7 @@ fn test() {
270
283
  `);
271
284
  runtime.load();
272
285
  runtime.execFunction('test');
273
- expect(runtime.getScore('lambda', 'iife')).toBe(10);
286
+ expect(runtime.getScore('lambda', 'runtime.iife')).toBe(10);
274
287
  });
275
288
  });
276
289
  //# sourceMappingURL=runtime.test.js.map