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
|
@@ -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
|
|
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",
|
|
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",
|
|
126
|
+
let x: int = scoreboard_get("#check_x", #test_score);
|
|
113
127
|
if (x > 5) {
|
|
114
|
-
scoreboard_set("#check_x",
|
|
128
|
+
scoreboard_set("#check_x", #result, 1);
|
|
115
129
|
} else {
|
|
116
|
-
scoreboard_set("#check_x",
|
|
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",
|
|
139
|
+
let time: int = scoreboard_get("#game", #timer);
|
|
126
140
|
if (time > 0) {
|
|
127
|
-
scoreboard_set("#game",
|
|
141
|
+
scoreboard_set("#game", #timer, time - 1);
|
|
128
142
|
}
|
|
129
143
|
if (time == 1) {
|
|
130
|
-
scoreboard_set("#game",
|
|
144
|
+
scoreboard_set("#game", #ended, 1);
|
|
131
145
|
}
|
|
132
146
|
}
|
|
133
147
|
fn start_game() {
|
|
134
|
-
scoreboard_set("#game",
|
|
135
|
-
scoreboard_set("#game",
|
|
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",
|
|
142
|
-
let b: int = scoreboard_get("#math",
|
|
143
|
-
scoreboard_set("#math",
|
|
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",
|
|
147
|
-
let y: int = scoreboard_get("#math",
|
|
148
|
-
scoreboard_set("#math",
|
|
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",
|
|
159
|
-
scoreboard_set("#chain",
|
|
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",
|
|
163
|
-
scoreboard_set("#chain",
|
|
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",
|
|
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",
|
|
197
|
+
scoreboard_set("#range", #counter, 0);
|
|
184
198
|
for i in 0..5 {
|
|
185
|
-
let c: int = scoreboard_get("#range",
|
|
186
|
-
scoreboard_set("#range",
|
|
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",
|
|
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",
|
|
205
|
-
2 => { scoreboard_set("#match",
|
|
206
|
-
3 => { scoreboard_set("#match",
|
|
207
|
-
_ => { scoreboard_set("#match",
|
|
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",
|
|
215
|
-
scoreboard_set("#wloop",
|
|
216
|
-
let i: int = scoreboard_get("#wloop",
|
|
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",
|
|
219
|
-
scoreboard_set("#wloop",
|
|
232
|
+
let s: int = scoreboard_get("#wloop", #steps);
|
|
233
|
+
scoreboard_set("#wloop", #steps, s + 1);
|
|
220
234
|
i = i - 1;
|
|
221
|
-
scoreboard_set("#wloop",
|
|
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",
|
|
242
|
+
let x: int = scoreboard_get("#boundary", #input);
|
|
229
243
|
if (x > 100) {
|
|
230
|
-
scoreboard_set("#boundary",
|
|
244
|
+
scoreboard_set("#boundary", #tier, 3);
|
|
231
245
|
} else {
|
|
232
246
|
if (x > 50) {
|
|
233
|
-
scoreboard_set("#boundary",
|
|
247
|
+
scoreboard_set("#boundary", #tier, 2);
|
|
234
248
|
} else {
|
|
235
249
|
if (x > 0) {
|
|
236
|
-
scoreboard_set("#boundary",
|
|
250
|
+
scoreboard_set("#boundary", #tier, 1);
|
|
237
251
|
} else {
|
|
238
|
-
scoreboard_set("#boundary",
|
|
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",
|
|
259
|
-
scoreboard_set("#order",
|
|
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",
|
|
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",
|
|
269
|
-
let v: int = scoreboard_get("#rmw",
|
|
270
|
-
scoreboard_set("#rmw",
|
|
271
|
-
v = scoreboard_get("#rmw",
|
|
272
|
-
scoreboard_set("#rmw",
|
|
273
|
-
v = scoreboard_get("#rmw",
|
|
274
|
-
scoreboard_set("#rmw",
|
|
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
|