redscript-mc 1.1.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 (63) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/__tests__/cli.test.js +138 -0
  3. package/dist/__tests__/codegen.test.js +25 -0
  4. package/dist/__tests__/e2e.test.js +190 -12
  5. package/dist/__tests__/lexer.test.js +12 -2
  6. package/dist/__tests__/lowering.test.js +164 -9
  7. package/dist/__tests__/mc-integration.test.js +145 -51
  8. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  9. package/dist/__tests__/parser.test.js +80 -0
  10. package/dist/__tests__/runtime.test.js +8 -8
  11. package/dist/__tests__/typechecker.test.js +158 -0
  12. package/dist/ast/types.d.ts +20 -1
  13. package/dist/codegen/mcfunction/index.js +30 -1
  14. package/dist/codegen/structure/index.js +25 -0
  15. package/dist/compile.d.ts +10 -0
  16. package/dist/compile.js +36 -5
  17. package/dist/events/types.d.ts +35 -0
  18. package/dist/events/types.js +59 -0
  19. package/dist/index.js +3 -2
  20. package/dist/ir/types.d.ts +4 -0
  21. package/dist/lexer/index.d.ts +1 -1
  22. package/dist/lexer/index.js +2 -0
  23. package/dist/lowering/index.d.ts +32 -1
  24. package/dist/lowering/index.js +439 -15
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +79 -10
  27. package/dist/typechecker/index.d.ts +17 -0
  28. package/dist/typechecker/index.js +343 -17
  29. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  30. package/editors/vscode/CHANGELOG.md +9 -0
  31. package/editors/vscode/out/extension.js +1144 -72
  32. package/editors/vscode/package-lock.json +2 -2
  33. package/editors/vscode/package.json +1 -1
  34. package/package.json +1 -1
  35. package/src/__tests__/cli.test.ts +166 -0
  36. package/src/__tests__/codegen.test.ts +27 -0
  37. package/src/__tests__/e2e.test.ts +201 -12
  38. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  39. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  40. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  41. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  42. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  43. package/src/__tests__/lexer.test.ts +14 -2
  44. package/src/__tests__/lowering.test.ts +178 -9
  45. package/src/__tests__/mc-integration.test.ts +166 -51
  46. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  47. package/src/__tests__/parser.test.ts +91 -5
  48. package/src/__tests__/runtime.test.ts +8 -8
  49. package/src/__tests__/typechecker.test.ts +171 -0
  50. package/src/ast/types.ts +25 -1
  51. package/src/codegen/mcfunction/index.ts +31 -1
  52. package/src/codegen/structure/index.ts +27 -0
  53. package/src/compile.ts +54 -6
  54. package/src/events/types.ts +69 -0
  55. package/src/index.ts +4 -3
  56. package/src/ir/types.ts +4 -0
  57. package/src/lexer/index.ts +3 -1
  58. package/src/lowering/index.ts +528 -16
  59. package/src/parser/index.ts +90 -12
  60. package/src/stdlib/README.md +34 -4
  61. package/src/stdlib/tags.mcrs +951 -0
  62. package/src/stdlib/timer.mcrs +54 -33
  63. package/src/typechecker/index.ts +404 -18
package/CHANGELOG.md CHANGED
@@ -2,6 +2,60 @@
2
2
 
3
3
  All notable changes to RedScript will be documented in this file.
4
4
 
5
+ ## [1.2.0] - 2026-03-12
6
+
7
+ ### Added
8
+ - `is` type narrowing for entity checks (`if (e is Player)`)
9
+ - `impl` blocks for struct methods
10
+ - Static method calls (`Type::method()`)
11
+ - Timer OOP API in stdlib
12
+ - `setTimeout(delay, callback)` builtin
13
+ - `setInterval(delay, callback)` builtin
14
+ - `clearInterval(id)` builtin
15
+ - `@on(Event)` static event system
16
+ - PlayerDeath, PlayerJoin, BlockBreak, EntityKill, ItemUse
17
+ - Automatic namespace prefixing for scoreboard objectives
18
+ - Comprehensive MC tag constants (313 tags)
19
+
20
+ ### Changed
21
+ - Stdlib timer functions now use OOP API
22
+
23
+ ## [1.1.0] - 2026-03-12
24
+
25
+ ### Language Features
26
+ - **Variable selector syntax**: `execute if entity p[x_rotation=-90..-45]` now works in foreach loops
27
+ - **New selector filters**: `x_rotation`, `y_rotation`, `x`, `y`, `z` for rotation and position checks
28
+ - **Duplicate binding detection**: Error when redeclaring foreach variables
29
+
30
+ ### Builtins
31
+ - `effect_clear(target, [effect])` — Clear all or specific effects
32
+ - `data_merge(target, nbt)` — Merge NBT data into entities
33
+
34
+ ### Standard Library
35
+ - `effects.mcrs` — Effect shortcuts (speed, strength, regen, buff_all...)
36
+ - `world.mcrs` — World/gamerule helpers (set_day, weather_clear, enable_keep_inventory...)
37
+ - `inventory.mcrs` — Inventory management (give_kit_warrior, clear_inventory...)
38
+ - `particles.mcrs` — Particle effects (hearts_at, flames, sparkles_at...)
39
+ - `spawn.mcrs` — Teleport utilities (teleport_to, gather_all, goto_lobby...)
40
+ - `teams.mcrs` — Team management (create_red_team, setup_two_teams...)
41
+ - `bossbar.mcrs` — Bossbar helpers (create_progress_bar, update_bar...)
42
+ - `interactions.mcrs` — Input detection (check_look_up, on_right_click, on_sneak_click...)
43
+
44
+ ### Bug Fixes
45
+ - Negative coordinates in summon/tp/particle now work correctly
46
+ - Stdlib particles use coordinates instead of selectors
47
+
48
+ ### Documentation
49
+ - Added tutorials: Zombie Survival, Capture the Flag, Parkour Race
50
+ - Added local debugging guide
51
+ - Added stdlib reference page
52
+ - Added Paper server testing guide
53
+
54
+ ### Community
55
+ - CONTRIBUTING.md with development guide
56
+ - GitHub issue/PR templates
57
+ - CHANGELOG.md
58
+
5
59
  ## [1.0.0] - 2026-03-12
6
60
 
7
61
  ### 🎉 Initial Release
@@ -66,6 +66,144 @@ describe('CLI API', () => {
66
66
  expect(result.ir.functions.filter(fn => fn.name === 'from_a')).toHaveLength(1);
67
67
  expect(result.ir.functions.filter(fn => fn.name === 'from_b')).toHaveLength(1);
68
68
  });
69
+ it('uses rs-prefixed scoreboard objectives for imported stdlib files', () => {
70
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-'));
71
+ const stdlibDir = path.join(tempDir, 'src', 'stdlib');
72
+ const stdlibPath = path.join(stdlibDir, 'timer.mcrs');
73
+ const mainPath = path.join(tempDir, 'main.mcrs');
74
+ fs.mkdirSync(stdlibDir, { recursive: true });
75
+ fs.writeFileSync(stdlibPath, 'fn tick_timer() { scoreboard_set("#rs", "timer_ticks", 1); }\n');
76
+ fs.writeFileSync(mainPath, 'import "./src/stdlib/timer.mcrs"\n\nfn main() { tick_timer(); }\n');
77
+ const source = fs.readFileSync(mainPath, 'utf-8');
78
+ const result = (0, index_1.compile)(source, { namespace: 'mygame', filePath: mainPath });
79
+ const tickTimer = result.files.find(file => file.path.endsWith('/tick_timer.mcfunction'));
80
+ expect(tickTimer?.content).toContain('scoreboard players set #rs rs.timer_ticks 1');
81
+ });
82
+ it('adds a call-site hash for stdlib internal scoreboard objectives', () => {
83
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-stdlib-hash-'));
84
+ const stdlibDir = path.join(tempDir, 'src', 'stdlib');
85
+ const stdlibPath = path.join(stdlibDir, 'timer.mcrs');
86
+ const mainPath = path.join(tempDir, 'main.mcrs');
87
+ fs.mkdirSync(stdlibDir, { recursive: true });
88
+ fs.writeFileSync(stdlibPath, [
89
+ 'fn timer_start(name: string, duration: int) {',
90
+ ' scoreboard_set("timer_ticks", #rs, duration);',
91
+ ' scoreboard_set("timer_active", #rs, 1);',
92
+ '}',
93
+ '',
94
+ ].join('\n'));
95
+ fs.writeFileSync(mainPath, [
96
+ 'import "./src/stdlib/timer.mcrs"',
97
+ '',
98
+ 'fn main() {',
99
+ ' timer_start("x", 100);',
100
+ ' timer_start("x", 100);',
101
+ '}',
102
+ '',
103
+ ].join('\n'));
104
+ const source = fs.readFileSync(mainPath, 'utf-8');
105
+ const result = (0, index_1.compile)(source, { namespace: 'mygame', filePath: mainPath });
106
+ const timerFns = result.files.filter(file => /timer_start__callsite_[0-9a-f]{4}\.mcfunction$/.test(file.path));
107
+ expect(timerFns).toHaveLength(2);
108
+ const objectives = timerFns
109
+ .flatMap(file => [...file.content.matchAll(/rs\._timer_([0-9a-f]{4})/g)].map(match => match[0]));
110
+ expect(new Set(objectives).size).toBe(2);
111
+ });
112
+ it('Timer::new creates timer', () => {
113
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-new-'));
114
+ const mainPath = path.join(tempDir, 'main.mcrs');
115
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
116
+ fs.writeFileSync(mainPath, [
117
+ `import "${timerPath}"`,
118
+ '',
119
+ 'fn main() {',
120
+ ' let timer: Timer = Timer::new(20);',
121
+ '}',
122
+ '',
123
+ ].join('\n'));
124
+ const source = fs.readFileSync(mainPath, 'utf-8');
125
+ const result = (0, index_1.compile)(source, { namespace: 'timernew', filePath: mainPath });
126
+ expect(result.typeErrors).toEqual([]);
127
+ const newFn = result.files.find(file => file.path.endsWith('/Timer_new.mcfunction'));
128
+ expect(newFn?.content).toContain('scoreboard players set timer_ticks rs 0');
129
+ expect(newFn?.content).toContain('scoreboard players set timer_active rs 0');
130
+ });
131
+ it('Timer.start/pause/reset', () => {
132
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-state-'));
133
+ const mainPath = path.join(tempDir, 'main.mcrs');
134
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
135
+ fs.writeFileSync(mainPath, [
136
+ `import "${timerPath}"`,
137
+ '',
138
+ 'fn main() {',
139
+ ' let timer: Timer = Timer::new(20);',
140
+ ' timer.start();',
141
+ ' timer.pause();',
142
+ ' timer.reset();',
143
+ '}',
144
+ '',
145
+ ].join('\n'));
146
+ const source = fs.readFileSync(mainPath, 'utf-8');
147
+ const result = (0, index_1.compile)(source, { namespace: 'timerstate', filePath: mainPath });
148
+ expect(result.typeErrors).toEqual([]);
149
+ const startFn = result.files.find(file => file.path.endsWith('/Timer_start.mcfunction'));
150
+ const pauseFn = result.files.find(file => file.path.endsWith('/Timer_pause.mcfunction'));
151
+ const resetFn = result.files.find(file => file.path.endsWith('/Timer_reset.mcfunction'));
152
+ expect(startFn?.content).toContain('scoreboard players set timer_active rs 1');
153
+ expect(pauseFn?.content).toContain('scoreboard players set timer_active rs 0');
154
+ expect(resetFn?.content).toContain('scoreboard players set timer_ticks rs 0');
155
+ });
156
+ it('Timer.done returns bool', () => {
157
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-done-'));
158
+ const mainPath = path.join(tempDir, 'main.mcrs');
159
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
160
+ fs.writeFileSync(mainPath, [
161
+ `import "${timerPath}"`,
162
+ '',
163
+ 'fn main() {',
164
+ ' let timer: Timer = Timer::new(20);',
165
+ ' let finished: bool = timer.done();',
166
+ ' if (finished) {',
167
+ ' say("done");',
168
+ ' }',
169
+ '}',
170
+ '',
171
+ ].join('\n'));
172
+ const source = fs.readFileSync(mainPath, 'utf-8');
173
+ const result = (0, index_1.compile)(source, { namespace: 'timerdone', filePath: mainPath });
174
+ expect(result.typeErrors).toEqual([]);
175
+ const doneFn = result.files.find(file => file.path.endsWith('/Timer_done.mcfunction'));
176
+ const mainFn = result.files.find(file => file.path.endsWith('/main.mcfunction'));
177
+ expect(doneFn?.content).toContain('scoreboard players get timer_ticks rs');
178
+ expect(doneFn?.content).toContain('return run scoreboard players get');
179
+ expect(mainFn?.content).toContain('execute if score $finished rs matches 1..');
180
+ });
181
+ it('Timer.tick increments', () => {
182
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-timer-tick-'));
183
+ const mainPath = path.join(tempDir, 'main.mcrs');
184
+ const timerPath = path.resolve(process.cwd(), 'src/stdlib/timer.mcrs');
185
+ fs.writeFileSync(mainPath, [
186
+ `import "${timerPath}"`,
187
+ '',
188
+ 'fn main() {',
189
+ ' let timer: Timer = Timer::new(20);',
190
+ ' timer.start();',
191
+ ' timer.tick();',
192
+ '}',
193
+ '',
194
+ ].join('\n'));
195
+ const source = fs.readFileSync(mainPath, 'utf-8');
196
+ const result = (0, index_1.compile)(source, { namespace: 'timertick', filePath: mainPath });
197
+ expect(result.typeErrors).toEqual([]);
198
+ const tickOutput = result.files
199
+ .filter(file => file.path.includes('/Timer_tick'))
200
+ .map(file => file.content)
201
+ .join('\n');
202
+ expect(tickOutput).toContain('scoreboard players get timer_active rs');
203
+ expect(tickOutput).toContain('scoreboard players get timer_ticks rs');
204
+ expect(tickOutput).toContain(' += $const_1 rs');
205
+ expect(tickOutput).toContain('execute store result score timer_ticks rs run scoreboard players get $_');
206
+ });
69
207
  });
70
208
  describe('compile()', () => {
71
209
  it('compiles simple source', () => {
@@ -117,5 +117,30 @@ describe('generateDatapack', () => {
117
117
  expect(json.criteria.trigger.trigger).toBe('minecraft:story/mine_diamond');
118
118
  expect(json.rewards.function).toBe('mypack:on_mine_diamond');
119
119
  });
120
+ it('generates static event dispatcher in __tick', () => {
121
+ const mod = {
122
+ namespace: 'mypack',
123
+ globals: [],
124
+ functions: [{
125
+ name: 'handle_death',
126
+ params: [],
127
+ locals: [],
128
+ blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
129
+ eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
130
+ }, {
131
+ name: 'handle_death_2',
132
+ params: [],
133
+ locals: [],
134
+ blocks: [{ label: 'entry', instrs: [], term: { op: 'return' } }],
135
+ eventHandler: { eventType: 'PlayerDeath', tag: 'rs.just_died' },
136
+ }],
137
+ };
138
+ const files = (0, mcfunction_1.generateDatapack)(mod);
139
+ const tickFn = files.find(f => f.path.includes('__tick.mcfunction'));
140
+ expect(tickFn).toBeDefined();
141
+ expect(tickFn.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death');
142
+ expect(tickFn.content).toContain('execute as @a[tag=rs.just_died] run function mypack:handle_death_2');
143
+ expect(tickFn.content).toContain('tag @a[tag=rs.just_died] remove rs.just_died');
144
+ });
120
145
  });
121
146
  //# sourceMappingURL=codegen.test.js.map
@@ -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', () => {
@@ -169,6 +175,10 @@ describe('Lexer', () => {
169
175
  const tokens = tokenize('=>');
170
176
  expect(kinds(tokens)).toEqual(['=>', 'eof']);
171
177
  });
178
+ it('tokenizes static method separators for impl methods', () => {
179
+ const tokens = tokenize('Point::new()');
180
+ expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof']);
181
+ });
172
182
  });
173
183
  describe('delimiters', () => {
174
184
  it('tokenizes all delimiters', () => {