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.
- package/CHANGELOG.md +59 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/cli.test.js +138 -0
- package/dist/__tests__/codegen.test.js +25 -0
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/e2e.test.js +190 -12
- package/dist/__tests__/lexer.test.js +31 -4
- package/dist/__tests__/lowering.test.js +172 -9
- package/dist/__tests__/mc-integration.test.js +145 -51
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/optimizer-advanced.test.js +3 -3
- package/dist/__tests__/parser.test.js +90 -0
- package/dist/__tests__/runtime.test.js +21 -8
- package/dist/__tests__/typechecker.test.js +188 -0
- package/dist/ast/types.d.ts +42 -3
- package/dist/cli.js +15 -10
- package/dist/codegen/mcfunction/index.js +30 -1
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +29 -2
- package/dist/compile.d.ts +11 -0
- package/dist/compile.js +40 -6
- package/dist/events/types.d.ts +35 -0
- package/dist/events/types.js +59 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +7 -3
- package/dist/ir/types.d.ts +4 -0
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +91 -1
- package/dist/lowering/index.d.ts +32 -1
- package/dist/lowering/index.js +476 -16
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +4 -0
- package/dist/parser/index.js +160 -26
- package/dist/typechecker/index.d.ts +19 -0
- package/dist/typechecker/index.js +392 -17
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/CHANGELOG.md +9 -0
- package/editors/vscode/icon.png +0 -0
- package/editors/vscode/out/extension.js +1144 -72
- package/editors/vscode/package-lock.json +2 -2
- package/editors/vscode/package.json +1 -1
- package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
- package/examples/spiral.mcrs +79 -0
- package/logo.png +0 -0
- package/package.json +1 -1
- package/src/__tests__/cli.test.ts +166 -0
- package/src/__tests__/codegen.test.ts +27 -0
- package/src/__tests__/dce.test.ts +129 -0
- package/src/__tests__/e2e.test.ts +201 -12
- package/src/__tests__/fixtures/event-test.mcrs +13 -0
- package/src/__tests__/fixtures/impl-test.mcrs +46 -0
- package/src/__tests__/fixtures/interval-test.mcrs +11 -0
- package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
- package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
- package/src/__tests__/lexer.test.ts +35 -4
- package/src/__tests__/lowering.test.ts +187 -9
- package/src/__tests__/mc-integration.test.ts +166 -51
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/optimizer-advanced.test.ts +3 -3
- package/src/__tests__/parser.test.ts +102 -5
- package/src/__tests__/runtime.test.ts +24 -8
- package/src/__tests__/typechecker.test.ts +204 -0
- package/src/ast/types.ts +39 -2
- package/src/cli.ts +24 -10
- package/src/codegen/mcfunction/index.ts +31 -1
- package/src/codegen/structure/index.ts +40 -2
- package/src/compile.ts +59 -7
- package/src/events/types.ts +69 -0
- package/src/index.ts +9 -4
- package/src/ir/types.ts +4 -0
- package/src/lexer/index.ts +105 -2
- package/src/lowering/index.ts +566 -18
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +187 -29
- package/src/stdlib/README.md +34 -4
- package/src/stdlib/tags.mcrs +951 -0
- package/src/stdlib/timer.mcrs +54 -33
- 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('
|
|
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(['+', '-', '*', '/', '%', '
|
|
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', () => {
|