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
|
@@ -146,6 +146,195 @@ fn main() {
|
|
|
146
146
|
})
|
|
147
147
|
})
|
|
148
148
|
|
|
149
|
+
describe('timer builtins', () => {
|
|
150
|
+
it('generates scheduled timer helper functions', () => {
|
|
151
|
+
const files = compile(`
|
|
152
|
+
fn main() {
|
|
153
|
+
let intervalId: int = setInterval(20, () => {
|
|
154
|
+
say("tick");
|
|
155
|
+
});
|
|
156
|
+
setTimeout(100, () => {
|
|
157
|
+
say("later");
|
|
158
|
+
});
|
|
159
|
+
clearInterval(intervalId);
|
|
160
|
+
}
|
|
161
|
+
`)
|
|
162
|
+
const mainFn = getFunction(files, 'main')
|
|
163
|
+
const intervalFn = getFunction(files, '__interval_0')
|
|
164
|
+
const intervalBodyFn = getFunction(files, '__interval_body_0')
|
|
165
|
+
const timeoutFn = getFunction(files, '__timeout_0')
|
|
166
|
+
expect(mainFn).toBeDefined()
|
|
167
|
+
expect(mainFn).toContain('schedule function test:__interval_0 20t')
|
|
168
|
+
expect(mainFn).toContain('schedule function test:__timeout_0 100t')
|
|
169
|
+
expect(mainFn).toContain('schedule clear test:__interval_0')
|
|
170
|
+
expect(intervalFn).toContain('function test:__interval_body_0')
|
|
171
|
+
expect(intervalFn).toContain('schedule function test:__interval_0 20t')
|
|
172
|
+
expect(intervalBodyFn).toContain('say tick')
|
|
173
|
+
expect(timeoutFn).toContain('say later')
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
describe('is type narrowing', () => {
|
|
178
|
+
it('type checks and compiles entity narrowing inside foreach blocks', () => {
|
|
179
|
+
const source = `
|
|
180
|
+
fn main() {
|
|
181
|
+
foreach (e in @e) {
|
|
182
|
+
if (e is Player) {
|
|
183
|
+
kill(e);
|
|
184
|
+
}
|
|
185
|
+
if (e is Zombie) {
|
|
186
|
+
kill(e);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
`
|
|
191
|
+
expect(typeCheck(source)).toEqual([])
|
|
192
|
+
|
|
193
|
+
const files = compile(source)
|
|
194
|
+
const mainFn = getFunction(files, 'main')
|
|
195
|
+
const foreachFn = getSubFunction(files, 'main', 'foreach_0')
|
|
196
|
+
const thenFiles = files.filter(file => file.path.includes('/main/then_') && file.content.includes('kill @s'))
|
|
197
|
+
|
|
198
|
+
expect(mainFn).toContain('execute as @e run function test:main/foreach_0')
|
|
199
|
+
expect(foreachFn).toContain('execute if entity @s[type=player] run function test:main/then_')
|
|
200
|
+
expect(foreachFn).toContain('execute if entity @s[type=zombie] run function test:main/then_')
|
|
201
|
+
expect(thenFiles).toHaveLength(2)
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
describe('impl blocks', () => {
|
|
206
|
+
it('compiles static and instance impl methods end to end', () => {
|
|
207
|
+
const source = `
|
|
208
|
+
struct Point { x: int, y: int }
|
|
209
|
+
|
|
210
|
+
impl Point {
|
|
211
|
+
fn new(x: int, y: int) -> Point {
|
|
212
|
+
return { x: x, y: y };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
fn distance(self) -> int {
|
|
216
|
+
return self.x + self.y;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fn main() {
|
|
221
|
+
let p: Point = Point::new(1, 2);
|
|
222
|
+
let d: int = p.distance();
|
|
223
|
+
say("\${d}");
|
|
224
|
+
}
|
|
225
|
+
`
|
|
226
|
+
expect(typeCheck(source)).toEqual([])
|
|
227
|
+
|
|
228
|
+
const files = compile(source)
|
|
229
|
+
const mainFn = getFunction(files, 'main')
|
|
230
|
+
const staticFn = getFunction(files, 'Point_new')
|
|
231
|
+
const instanceFn = getFunction(files, 'Point_distance')
|
|
232
|
+
|
|
233
|
+
expect(mainFn).toContain('function test:Point_new')
|
|
234
|
+
expect(mainFn).toContain('function test:Point_distance')
|
|
235
|
+
expect(staticFn).toBeDefined()
|
|
236
|
+
expect(instanceFn).toBeDefined()
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('namespace prefixing', () => {
|
|
241
|
+
it('prefixes user objectives but preserves mc_name and qualified objectives', () => {
|
|
242
|
+
const files = compile(`
|
|
243
|
+
fn main() {
|
|
244
|
+
scoreboard_set("timer", #rs, 100);
|
|
245
|
+
scoreboard_set(@s, "timer", 100);
|
|
246
|
+
scoreboard_set(@s, #health, 20);
|
|
247
|
+
scoreboard_set(@s, "custom.timer", 1);
|
|
248
|
+
}
|
|
249
|
+
`, 'pack')
|
|
250
|
+
const mainFn = getFunction(files, 'main')
|
|
251
|
+
expect(mainFn).toContain('scoreboard players set timer rs 100')
|
|
252
|
+
expect(mainFn).toContain('scoreboard players set @s pack.timer 100')
|
|
253
|
+
expect(mainFn).toContain('scoreboard players set @s health 20')
|
|
254
|
+
expect(mainFn).toContain('scoreboard players set @s custom.timer 1')
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('Timer OOP API', () => {
|
|
259
|
+
it('compiles the Timer impl API end to end', () => {
|
|
260
|
+
const source = `
|
|
261
|
+
struct Timer {
|
|
262
|
+
_id: int,
|
|
263
|
+
_duration: int
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
impl Timer {
|
|
267
|
+
fn new(duration: int) -> Timer {
|
|
268
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
269
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
270
|
+
return { _id: 0, _duration: duration };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
fn start(self) {
|
|
274
|
+
scoreboard_set("timer_active", #rs, 1);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
fn pause(self) {
|
|
278
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn reset(self) {
|
|
282
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
fn done(self) -> bool {
|
|
286
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
287
|
+
return ticks >= self._duration;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
fn tick(self) {
|
|
291
|
+
let active: int = scoreboard_get("timer_active", #rs);
|
|
292
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
293
|
+
|
|
294
|
+
if (active == 1) {
|
|
295
|
+
if (ticks < self._duration) {
|
|
296
|
+
scoreboard_set("timer_ticks", #rs, ticks + 1);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
fn main() {
|
|
303
|
+
let t: Timer = Timer::new(100);
|
|
304
|
+
t.start();
|
|
305
|
+
t.tick();
|
|
306
|
+
let finished: bool = t.done();
|
|
307
|
+
if (finished) {
|
|
308
|
+
say("done");
|
|
309
|
+
}
|
|
310
|
+
t.pause();
|
|
311
|
+
t.reset();
|
|
312
|
+
}
|
|
313
|
+
`
|
|
314
|
+
expect(typeCheck(source)).toEqual([])
|
|
315
|
+
|
|
316
|
+
const files = compile(source, 'timerapi')
|
|
317
|
+
const mainFn = getFunction(files, 'main')
|
|
318
|
+
const newFn = getFunction(files, 'Timer_new')
|
|
319
|
+
const startFn = getFunction(files, 'Timer_start')
|
|
320
|
+
const tickFn = getFunction(files, 'Timer_tick')
|
|
321
|
+
const doneFn = getFunction(files, 'Timer_done')
|
|
322
|
+
const pauseFn = getFunction(files, 'Timer_pause')
|
|
323
|
+
const resetFn = getFunction(files, 'Timer_reset')
|
|
324
|
+
|
|
325
|
+
expect(mainFn).toContain('function timerapi:Timer_new')
|
|
326
|
+
expect(mainFn).toContain('function timerapi:Timer_start')
|
|
327
|
+
expect(mainFn).toContain('function timerapi:Timer_tick')
|
|
328
|
+
expect(mainFn).toContain('function timerapi:Timer_done')
|
|
329
|
+
expect(newFn).toContain('scoreboard players set timer_ticks rs 0')
|
|
330
|
+
expect(startFn).toContain('scoreboard players set timer_active rs 1')
|
|
331
|
+
expect(tickFn).toContain('scoreboard players get timer_active rs')
|
|
332
|
+
expect(doneFn).toContain('scoreboard players get timer_ticks rs')
|
|
333
|
+
expect(pauseFn).toContain('scoreboard players set timer_active rs 0')
|
|
334
|
+
expect(resetFn).toContain('scoreboard players set timer_ticks rs 0')
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
149
338
|
describe('advancement event decorators', () => {
|
|
150
339
|
it('generates advancement json with reward function path', () => {
|
|
151
340
|
const source = `
|
|
@@ -309,12 +498,12 @@ fn test() {
|
|
|
309
498
|
}
|
|
310
499
|
`
|
|
311
500
|
const fn = getFunction(compile(source), 'test')!
|
|
312
|
-
expect(fn).toContain('scoreboard objectives setdisplay sidebar kills')
|
|
313
|
-
expect(fn).toContain('scoreboard objectives setdisplay list coins')
|
|
314
|
-
expect(fn).toContain('scoreboard objectives setdisplay belowName hp')
|
|
501
|
+
expect(fn).toContain('scoreboard objectives setdisplay sidebar test.kills')
|
|
502
|
+
expect(fn).toContain('scoreboard objectives setdisplay list test.coins')
|
|
503
|
+
expect(fn).toContain('scoreboard objectives setdisplay belowName test.hp')
|
|
315
504
|
expect(fn).toContain('scoreboard objectives setdisplay sidebar')
|
|
316
|
-
expect(fn).toContain('scoreboard objectives add kills playerKillCount "Kill Count"')
|
|
317
|
-
expect(fn).toContain('scoreboard objectives remove kills')
|
|
505
|
+
expect(fn).toContain('scoreboard objectives add test.kills playerKillCount "Kill Count"')
|
|
506
|
+
expect(fn).toContain('scoreboard objectives remove test.kills')
|
|
318
507
|
})
|
|
319
508
|
|
|
320
509
|
it('compiles bossbar builtins', () => {
|
|
@@ -536,7 +725,7 @@ fn test() -> int {
|
|
|
536
725
|
const fn = getFunction(files, 'test')
|
|
537
726
|
expect(fn).toBeDefined()
|
|
538
727
|
expect(fn).toContain('execute store result score')
|
|
539
|
-
expect(fn).toContain('scoreboard players get PlayerName kill_count')
|
|
728
|
+
expect(fn).toContain('scoreboard players get PlayerName test.kill_count')
|
|
540
729
|
})
|
|
541
730
|
|
|
542
731
|
it('compiles scoreboard_get with @s selector', () => {
|
|
@@ -549,7 +738,7 @@ fn test() -> int {
|
|
|
549
738
|
const files = compile(source)
|
|
550
739
|
const fn = getFunction(files, 'test')
|
|
551
740
|
expect(fn).toBeDefined()
|
|
552
|
-
expect(fn).toContain('scoreboard players get @s kill_count')
|
|
741
|
+
expect(fn).toContain('scoreboard players get @s test.kill_count')
|
|
553
742
|
})
|
|
554
743
|
|
|
555
744
|
it('compiles scoreboard_set with constant value', () => {
|
|
@@ -561,7 +750,7 @@ fn test() {
|
|
|
561
750
|
const files = compile(source)
|
|
562
751
|
const fn = getFunction(files, 'test')
|
|
563
752
|
expect(fn).toBeDefined()
|
|
564
|
-
expect(fn).toContain('scoreboard players set PlayerName kill_count 100')
|
|
753
|
+
expect(fn).toContain('scoreboard players set PlayerName test.kill_count 100')
|
|
565
754
|
})
|
|
566
755
|
|
|
567
756
|
it('compiles scoreboard_set with variable value', () => {
|
|
@@ -576,7 +765,7 @@ fn test() {
|
|
|
576
765
|
.filter(f => f.path.includes('test'))
|
|
577
766
|
.map(f => f.content)
|
|
578
767
|
.join('\n')
|
|
579
|
-
expect(allContent).toContain('execute store result score @s score')
|
|
768
|
+
expect(allContent).toContain('execute store result score @s test.score')
|
|
580
769
|
})
|
|
581
770
|
|
|
582
771
|
it('compiles score() as expression', () => {
|
|
@@ -604,7 +793,7 @@ fn double_score() -> int {
|
|
|
604
793
|
const files = compile(source)
|
|
605
794
|
const fn = getFunction(files, 'double_score')
|
|
606
795
|
expect(fn).toBeDefined()
|
|
607
|
-
expect(fn).toContain('scoreboard players get @s points')
|
|
796
|
+
expect(fn).toContain('scoreboard players get @s test.points')
|
|
608
797
|
})
|
|
609
798
|
})
|
|
610
799
|
|
|
@@ -1481,10 +1670,10 @@ fn heal(amount: int) {
|
|
|
1481
1670
|
|
|
1482
1671
|
describe('backward compat: string objective still works', () => {
|
|
1483
1672
|
const source = `fn test() { let x: int = scoreboard_get(@s, "kills"); }`
|
|
1484
|
-
it('
|
|
1673
|
+
it('prefixes plain string objectives with the active namespace', () => {
|
|
1485
1674
|
const files = compile(source, 'compat')
|
|
1486
1675
|
const fn = getFunction(files, 'test')
|
|
1487
|
-
expect(fn).toContain('scoreboard players get @s kills')
|
|
1676
|
+
expect(fn).toContain('scoreboard players get @s compat.kills')
|
|
1488
1677
|
})
|
|
1489
1678
|
})
|
|
1490
1679
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Test @on(PlayerDeath) event system
|
|
2
|
+
// Since we can't easily kill players in test, we manually set the tag
|
|
3
|
+
|
|
4
|
+
@on(PlayerDeath)
|
|
5
|
+
fn handle_death(player: Player) {
|
|
6
|
+
say("Player died event triggered!");
|
|
7
|
+
scoreboard_set("#event_test", "death_count", scoreboard_get("#event_test", "death_count") + 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
fn trigger_fake_death() {
|
|
11
|
+
// Manually add the death tag to simulate event
|
|
12
|
+
tag_add(@a, "rs.just_died");
|
|
13
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
struct timer {
|
|
2
|
+
_id: int,
|
|
3
|
+
_duration: int
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
impl timer {
|
|
7
|
+
fn new(duration: int) -> timer {
|
|
8
|
+
scoreboard_set("timer_ticks", #rs, 0);
|
|
9
|
+
scoreboard_set("timer_active", #rs, 0);
|
|
10
|
+
return { _id: 0, _duration: duration };
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
fn start(self) {
|
|
14
|
+
scoreboard_set("timer_active", #rs, 1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
fn tick(self) {
|
|
18
|
+
let active: int = scoreboard_get("timer_active", #rs);
|
|
19
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
20
|
+
|
|
21
|
+
if (active == 1) {
|
|
22
|
+
if (ticks < self._duration) {
|
|
23
|
+
scoreboard_set("timer_ticks", #rs, ticks + 1);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
fn done(self) -> bool {
|
|
29
|
+
let ticks: int = scoreboard_get("timer_ticks", #rs);
|
|
30
|
+
return ticks >= 3;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
fn test() {
|
|
35
|
+
let timer_value: timer = timer::new(3);
|
|
36
|
+
timer_value.start();
|
|
37
|
+
timer_value.tick();
|
|
38
|
+
timer_value.tick();
|
|
39
|
+
timer_value.tick();
|
|
40
|
+
let finished: bool = timer_value.done();
|
|
41
|
+
if (finished) {
|
|
42
|
+
scoreboard_set("#impl", #done, 1);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
scoreboard_set("#impl", #done, 1);
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
fn check_types() {
|
|
2
|
+
scoreboard_set("#is_check", #players, 0);
|
|
3
|
+
scoreboard_set("#is_check", #zombies, 0);
|
|
4
|
+
|
|
5
|
+
foreach (e in @e[type=zombie,tag=is_check_target]) {
|
|
6
|
+
if (e is Player) {
|
|
7
|
+
let players: int = scoreboard_get("#is_check", #players);
|
|
8
|
+
scoreboard_set("#is_check", #players, players + 1);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (e is Zombie) {
|
|
12
|
+
let players: int = scoreboard_get("#is_check", #players);
|
|
13
|
+
scoreboard_set("#is_check", #players, players);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let zombies: int = scoreboard_get("#is_check", #zombies);
|
|
17
|
+
scoreboard_set("#is_check", #zombies, zombies + 1);
|
|
18
|
+
kill(e);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -11,10 +11,17 @@ function kinds(tokens: Token[]): TokenKind[] {
|
|
|
11
11
|
describe('Lexer', () => {
|
|
12
12
|
describe('keywords', () => {
|
|
13
13
|
it('recognizes all keywords', () => {
|
|
14
|
-
const tokens = tokenize('fn let const if else while for foreach match return as at in struct enum trigger namespace')
|
|
14
|
+
const tokens = tokenize('fn let const if else while for foreach match return as at in is struct impl enum trigger namespace')
|
|
15
15
|
expect(kinds(tokens)).toEqual([
|
|
16
16
|
'fn', 'let', 'const', 'if', 'else', 'while', 'for', 'foreach', 'match',
|
|
17
|
-
'return', 'as', 'at', 'in', 'struct', 'enum', 'trigger', 'namespace', 'eof'
|
|
17
|
+
'return', 'as', 'at', 'in', 'is', 'struct', 'impl', 'enum', 'trigger', 'namespace', 'eof'
|
|
18
|
+
])
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('tokenizes is-check and impl syntax with their dedicated keywords', () => {
|
|
22
|
+
const tokens = tokenize('if (e is Player) { } impl Point { }')
|
|
23
|
+
expect(kinds(tokens)).toEqual([
|
|
24
|
+
'if', '(', 'ident', 'is', 'ident', ')', '{', '}', 'impl', 'ident', '{', '}', 'eof',
|
|
18
25
|
])
|
|
19
26
|
})
|
|
20
27
|
|
|
@@ -81,6 +88,14 @@ describe('Lexer', () => {
|
|
|
81
88
|
])
|
|
82
89
|
})
|
|
83
90
|
|
|
91
|
+
it('tokenizes f-strings as a dedicated token', () => {
|
|
92
|
+
const tokens = tokenize('f"Hello {name}!"')
|
|
93
|
+
expect(tokens.map(t => [t.kind, t.value])).toEqual([
|
|
94
|
+
['f_string', 'Hello {name}!'],
|
|
95
|
+
['eof', ''],
|
|
96
|
+
])
|
|
97
|
+
})
|
|
98
|
+
|
|
84
99
|
it('tokenizes byte literals (b suffix)', () => {
|
|
85
100
|
const tokens = tokenize('20b 0B 127b')
|
|
86
101
|
expect(tokens.map(t => [t.kind, t.value])).toEqual([
|
|
@@ -163,8 +178,19 @@ describe('Lexer', () => {
|
|
|
163
178
|
|
|
164
179
|
describe('operators', () => {
|
|
165
180
|
it('tokenizes arithmetic operators', () => {
|
|
166
|
-
const tokens = tokenize('+ - * / %
|
|
167
|
-
expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', '
|
|
181
|
+
const tokens = tokenize('+ - * / %')
|
|
182
|
+
expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', 'eof'])
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('tokenizes relative and local coordinates', () => {
|
|
186
|
+
const tokens = tokenize('~ ~5 ~-3 ^ ^10 ^-2')
|
|
187
|
+
expect(kinds(tokens)).toEqual(['rel_coord', 'rel_coord', 'rel_coord', 'local_coord', 'local_coord', 'local_coord', 'eof'])
|
|
188
|
+
expect(tokens[0].value).toBe('~')
|
|
189
|
+
expect(tokens[1].value).toBe('~5')
|
|
190
|
+
expect(tokens[2].value).toBe('~-3')
|
|
191
|
+
expect(tokens[3].value).toBe('^')
|
|
192
|
+
expect(tokens[4].value).toBe('^10')
|
|
193
|
+
expect(tokens[5].value).toBe('^-2')
|
|
168
194
|
})
|
|
169
195
|
|
|
170
196
|
it('tokenizes comparison operators', () => {
|
|
@@ -191,6 +217,11 @@ describe('Lexer', () => {
|
|
|
191
217
|
const tokens = tokenize('=>')
|
|
192
218
|
expect(kinds(tokens)).toEqual(['=>', 'eof'])
|
|
193
219
|
})
|
|
220
|
+
|
|
221
|
+
it('tokenizes static method separators for impl methods', () => {
|
|
222
|
+
const tokens = tokenize('Point::new()')
|
|
223
|
+
expect(kinds(tokens)).toEqual(['ident', '::', 'ident', '(', ')', 'eof'])
|
|
224
|
+
})
|
|
194
225
|
})
|
|
195
226
|
|
|
196
227
|
describe('delimiters', () => {
|