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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,65 @@
|
|
|
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
|
+
- Runtime f-strings for output functions
|
|
12
|
+
- Timer OOP API in stdlib
|
|
13
|
+
- `setTimeout(delay, callback)` builtin
|
|
14
|
+
- `setInterval(delay, callback)` builtin
|
|
15
|
+
- `clearInterval(id)` builtin
|
|
16
|
+
- `@on(Event)` static event system
|
|
17
|
+
- PlayerDeath, PlayerJoin, BlockBreak, EntityKill, ItemUse
|
|
18
|
+
- Dead code elimination optimizer pass
|
|
19
|
+
- Automatic namespace prefixing for scoreboard objectives
|
|
20
|
+
- Comprehensive MC tag constants (313 tags)
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Stdlib timer functions now use OOP API
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
- Updated README and docs site for the v1.2 language, stdlib, and builtins changes
|
|
27
|
+
|
|
28
|
+
## [1.1.0] - 2026-03-12
|
|
29
|
+
|
|
30
|
+
### Language Features
|
|
31
|
+
- **Variable selector syntax**: `execute if entity p[x_rotation=-90..-45]` now works in foreach loops
|
|
32
|
+
- **New selector filters**: `x_rotation`, `y_rotation`, `x`, `y`, `z` for rotation and position checks
|
|
33
|
+
- **Duplicate binding detection**: Error when redeclaring foreach variables
|
|
34
|
+
|
|
35
|
+
### Builtins
|
|
36
|
+
- `effect_clear(target, [effect])` — Clear all or specific effects
|
|
37
|
+
- `data_merge(target, nbt)` — Merge NBT data into entities
|
|
38
|
+
|
|
39
|
+
### Standard Library
|
|
40
|
+
- `effects.mcrs` — Effect shortcuts (speed, strength, regen, buff_all...)
|
|
41
|
+
- `world.mcrs` — World/gamerule helpers (set_day, weather_clear, enable_keep_inventory...)
|
|
42
|
+
- `inventory.mcrs` — Inventory management (give_kit_warrior, clear_inventory...)
|
|
43
|
+
- `particles.mcrs` — Particle effects (hearts_at, flames, sparkles_at...)
|
|
44
|
+
- `spawn.mcrs` — Teleport utilities (teleport_to, gather_all, goto_lobby...)
|
|
45
|
+
- `teams.mcrs` — Team management (create_red_team, setup_two_teams...)
|
|
46
|
+
- `bossbar.mcrs` — Bossbar helpers (create_progress_bar, update_bar...)
|
|
47
|
+
- `interactions.mcrs` — Input detection (check_look_up, on_right_click, on_sneak_click...)
|
|
48
|
+
|
|
49
|
+
### Bug Fixes
|
|
50
|
+
- Negative coordinates in summon/tp/particle now work correctly
|
|
51
|
+
- Stdlib particles use coordinates instead of selectors
|
|
52
|
+
|
|
53
|
+
### Documentation
|
|
54
|
+
- Added tutorials: Zombie Survival, Capture the Flag, Parkour Race
|
|
55
|
+
- Added local debugging guide
|
|
56
|
+
- Added stdlib reference page
|
|
57
|
+
- Added Paper server testing guide
|
|
58
|
+
|
|
59
|
+
### Community
|
|
60
|
+
- CONTRIBUTING.md with development guide
|
|
61
|
+
- GitHub issue/PR templates
|
|
62
|
+
- CHANGELOG.md
|
|
63
|
+
|
|
5
64
|
## [1.0.0] - 2026-03-12
|
|
6
65
|
|
|
7
66
|
### 🎉 Initial Release
|
package/README.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="./logo.png" alt="RedScript Logo" width="64" />
|
|
4
|
+
|
|
5
|
+
<img src="https://img.shields.io/badge/RedScript-1.2-red?style=for-the-badge&logo=minecraft&logoColor=white" alt="RedScript" />
|
|
4
6
|
|
|
5
7
|
**A typed scripting language that compiles to Minecraft datapacks.**
|
|
6
8
|
|
|
7
9
|
Write clean game logic. RedScript handles the scoreboard spaghetti.
|
|
8
10
|
|
|
9
11
|
[](https://github.com/bkmashiro/redscript/actions/workflows/ci.yml)
|
|
10
|
-
[](https://github.com/bkmashiro/redscript)
|
|
11
13
|
[](https://www.npmjs.com/package/redscript-mc)
|
|
12
14
|
[](https://www.npmjs.com/package/redscript-mc)
|
|
13
15
|
[](https://marketplace.visualstudio.com/items?itemName=bkmashiro.redscript-vscode)
|
|
@@ -70,6 +72,19 @@ One file. Compiles to a ready-to-use datapack in seconds.
|
|
|
70
72
|
|
|
71
73
|
---
|
|
72
74
|
|
|
75
|
+
### What's New in v1.2
|
|
76
|
+
|
|
77
|
+
- `impl` blocks and methods for object-style APIs on structs
|
|
78
|
+
- `is` type narrowing for safer entity checks
|
|
79
|
+
- Static events with `@on(Event)`
|
|
80
|
+
- Runtime f-strings for `say`, `title`, `actionbar`, and related output
|
|
81
|
+
- Timer OOP API with `Timer::new(...)` and instance methods
|
|
82
|
+
- `setTimeout(...)` and `setInterval(...)` scheduling helpers
|
|
83
|
+
- Dead code elimination in the optimizer
|
|
84
|
+
- 313 Minecraft tag constants in the standard library
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
73
88
|
### Quick Start
|
|
74
89
|
|
|
75
90
|
#### Option 1: Online IDE (No Install)
|
|
@@ -83,19 +98,32 @@ One file. Compiles to a ready-to-use datapack in seconds.
|
|
|
83
98
|
|
|
84
99
|
#### Option 3: CLI
|
|
85
100
|
|
|
101
|
+
```mcrs
|
|
102
|
+
struct Timer { _id: int; duration: int; }
|
|
103
|
+
|
|
104
|
+
impl Timer {
|
|
105
|
+
fn new(duration: int): Timer {
|
|
106
|
+
return Timer { _id: 0, duration: duration };
|
|
107
|
+
}
|
|
108
|
+
fn done(self): bool { return true; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@on(PlayerJoin)
|
|
112
|
+
fn welcome(player: Player) {
|
|
113
|
+
say(f"Welcome {player}!");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@tick fn game_loop() {
|
|
117
|
+
let timer = Timer::new(100);
|
|
118
|
+
setTimeout(200, () => { say("Delayed!"); });
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
86
122
|
```bash
|
|
87
123
|
npm install -g redscript-mc
|
|
88
124
|
redscript compile game.mcrs -o ./my-datapack
|
|
89
125
|
```
|
|
90
126
|
|
|
91
|
-
```
|
|
92
|
-
✓ Compiled pvp_game.mcrs
|
|
93
|
-
Namespace : pvp_game
|
|
94
|
-
Functions : 7
|
|
95
|
-
Commands : 34 → 28 (optimizer: −18%)
|
|
96
|
-
Output : ./my-datapack/
|
|
97
|
-
```
|
|
98
|
-
|
|
99
127
|
#### Deploy
|
|
100
128
|
|
|
101
129
|
Drop the output folder into your world's `datapacks/` directory and run `/reload`. Done.
|
|
@@ -261,6 +289,21 @@ import "stdlib/mobs.mcrs" // ZOMBIE, SKELETON, CREEPER, ... (60+ constants
|
|
|
261
289
|
|
|
262
290
|
---
|
|
263
291
|
|
|
292
|
+
### Changelog Highlights
|
|
293
|
+
|
|
294
|
+
#### v1.2.0
|
|
295
|
+
|
|
296
|
+
- Added `impl` blocks, methods, and static constructors
|
|
297
|
+
- Added `is` type narrowing for entity-safe control flow
|
|
298
|
+
- Added `@on(Event)` static events and callback scheduling builtins
|
|
299
|
+
- Added runtime f-strings for output functions
|
|
300
|
+
- Expanded stdlib with Timer OOP APIs and 313 MC tag constants
|
|
301
|
+
- Improved optimization with dead code elimination
|
|
302
|
+
|
|
303
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full release notes.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
264
307
|
<div align="center">
|
|
265
308
|
|
|
266
309
|
MIT License · Copyright © 2026 [bkmashiro](https://github.com/bkmashiro)
|
package/README.zh.md
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
<img src="
|
|
3
|
+
<img src="./logo.png" alt="RedScript Logo" width="64" />
|
|
4
|
+
|
|
5
|
+
<img src="https://img.shields.io/badge/RedScript-1.2-red?style=for-the-badge&logo=minecraft&logoColor=white" alt="RedScript" />
|
|
4
6
|
|
|
5
7
|
**一个编译到 Minecraft Datapack 的类型化脚本语言。**
|
|
6
8
|
|
|
7
9
|
写干净的游戏逻辑,把记分板的面条代码交给 RedScript 处理。
|
|
8
10
|
|
|
9
11
|
[](https://github.com/bkmashiro/redscript/actions/workflows/ci.yml)
|
|
10
|
-
[](https://github.com/bkmashiro/redscript)
|
|
11
13
|
[](https://www.npmjs.com/package/redscript-mc)
|
|
12
14
|
[](https://www.npmjs.com/package/redscript-mc)
|
|
13
15
|
[](https://marketplace.visualstudio.com/items?itemName=bkmashiro.redscript-vscode)
|
|
@@ -70,6 +72,19 @@ fn on_kill() {
|
|
|
70
72
|
|
|
71
73
|
---
|
|
72
74
|
|
|
75
|
+
### v1.2 新增内容
|
|
76
|
+
|
|
77
|
+
- `impl` 块与方法,支持围绕结构体构建面向对象风格 API
|
|
78
|
+
- `is` 类型收窄,实体判断更安全
|
|
79
|
+
- 使用 `@on(Event)` 的静态事件系统
|
|
80
|
+
- 面向运行时输出的 f-string
|
|
81
|
+
- `Timer::new(...)` 与实例方法组成的 Timer OOP API
|
|
82
|
+
- `setTimeout(...)` 与 `setInterval(...)` 调度辅助函数
|
|
83
|
+
- 优化器中的死代码消除
|
|
84
|
+
- 标准库新增 313 个 Minecraft 标签常量
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
73
88
|
### 快速开始
|
|
74
89
|
|
|
75
90
|
#### 方式 1:在线 IDE(无需安装)
|
|
@@ -83,19 +98,32 @@ fn on_kill() {
|
|
|
83
98
|
|
|
84
99
|
#### 方式 3:命令行
|
|
85
100
|
|
|
101
|
+
```mcrs
|
|
102
|
+
struct Timer { _id: int; duration: int; }
|
|
103
|
+
|
|
104
|
+
impl Timer {
|
|
105
|
+
fn new(duration: int): Timer {
|
|
106
|
+
return Timer { _id: 0, duration: duration };
|
|
107
|
+
}
|
|
108
|
+
fn done(self): bool { return true; }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@on(PlayerJoin)
|
|
112
|
+
fn welcome(player: Player) {
|
|
113
|
+
say(f"Welcome {player}!");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@tick fn game_loop() {
|
|
117
|
+
let timer = Timer::new(100);
|
|
118
|
+
setTimeout(200, () => { say("Delayed!"); });
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
86
122
|
```bash
|
|
87
123
|
npm install -g redscript-mc
|
|
88
124
|
redscript compile game.mcrs -o ./my-datapack
|
|
89
125
|
```
|
|
90
126
|
|
|
91
|
-
```
|
|
92
|
-
✓ 已编译 pvp_game.mcrs
|
|
93
|
-
命名空间 : pvp_game
|
|
94
|
-
函数数量 : 7
|
|
95
|
-
命令数量 : 34 → 28 (优化器节省了 18%)
|
|
96
|
-
输出目录 : ./my-datapack/
|
|
97
|
-
```
|
|
98
|
-
|
|
99
127
|
#### 部署
|
|
100
128
|
|
|
101
129
|
把输出文件夹丢进存档的 `datapacks/` 目录,游戏内跑 `/reload`,完成。
|
|
@@ -261,6 +289,21 @@ import "stdlib/mobs.mcrs" // ZOMBIE, SKELETON, CREEPER ... (60+ 实体常
|
|
|
261
289
|
|
|
262
290
|
---
|
|
263
291
|
|
|
292
|
+
### 更新日志亮点
|
|
293
|
+
|
|
294
|
+
#### v1.2.0
|
|
295
|
+
|
|
296
|
+
- 新增 `impl` 块、实例方法与静态构造函数
|
|
297
|
+
- 新增 `is` 类型收窄,提升实体相关控制流的类型安全
|
|
298
|
+
- 新增 `@on(Event)` 静态事件与回调调度内置函数
|
|
299
|
+
- 新增运行时输出用 f-string
|
|
300
|
+
- 标准库补充 Timer OOP API 与 313 个 MC 标签常量
|
|
301
|
+
- 优化器支持死代码消除
|
|
302
|
+
|
|
303
|
+
完整发布说明见 [CHANGELOG.md](./CHANGELOG.md)。
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
264
307
|
<div align="center">
|
|
265
308
|
|
|
266
309
|
MIT License · Copyright © 2026 [bkmashiro](https://github.com/bkmashiro)
|
|
@@ -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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const fs = __importStar(require("fs"));
|
|
37
|
+
const os = __importStar(require("os"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const child_process_1 = require("child_process");
|
|
40
|
+
const index_1 = require("../index");
|
|
41
|
+
function getFileContent(files, suffix) {
|
|
42
|
+
const file = files.find(candidate => candidate.path.endsWith(suffix));
|
|
43
|
+
if (!file) {
|
|
44
|
+
throw new Error(`Missing file: ${suffix}`);
|
|
45
|
+
}
|
|
46
|
+
return file.content;
|
|
47
|
+
}
|
|
48
|
+
describe('AST dead code elimination', () => {
|
|
49
|
+
it('removes unused functions reachable from entry points', () => {
|
|
50
|
+
const source = `
|
|
51
|
+
fn unused() { say("never called"); }
|
|
52
|
+
fn used() { say("called"); }
|
|
53
|
+
@tick fn main() { used(); }
|
|
54
|
+
`;
|
|
55
|
+
const result = (0, index_1.compile)(source, { namespace: 'test' });
|
|
56
|
+
expect(result.ast.declarations.map(fn => fn.name)).toEqual(['used', 'main']);
|
|
57
|
+
expect(result.ir.functions.some(fn => fn.name === 'unused')).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it('removes unused local variables from the AST body', () => {
|
|
60
|
+
const source = `
|
|
61
|
+
fn helper() {
|
|
62
|
+
let unused: int = 10;
|
|
63
|
+
let used: int = 20;
|
|
64
|
+
say_int(used);
|
|
65
|
+
}
|
|
66
|
+
@tick fn main() { helper(); }
|
|
67
|
+
`;
|
|
68
|
+
const result = (0, index_1.compile)(source, { namespace: 'test' });
|
|
69
|
+
const helper = result.ast.declarations.find(fn => fn.name === 'helper');
|
|
70
|
+
expect(helper?.body.filter(stmt => stmt.kind === 'let')).toHaveLength(1);
|
|
71
|
+
expect(helper?.body.some(stmt => stmt.kind === 'let' && stmt.name === 'unused')).toBe(false);
|
|
72
|
+
});
|
|
73
|
+
it('removes unused constants', () => {
|
|
74
|
+
const source = `
|
|
75
|
+
const UNUSED: int = 10;
|
|
76
|
+
const USED: int = 20;
|
|
77
|
+
|
|
78
|
+
@tick fn main() {
|
|
79
|
+
say_int(USED);
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
const result = (0, index_1.compile)(source, { namespace: 'test' });
|
|
83
|
+
expect(result.ast.consts.map(constDecl => constDecl.name)).toEqual(['USED']);
|
|
84
|
+
});
|
|
85
|
+
it('eliminates dead branches with constant conditions', () => {
|
|
86
|
+
const source = `
|
|
87
|
+
@tick fn main() {
|
|
88
|
+
if (false) {
|
|
89
|
+
say("dead code");
|
|
90
|
+
} else {
|
|
91
|
+
say("live code");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
`;
|
|
95
|
+
const result = (0, index_1.compile)(source, { namespace: 'test' });
|
|
96
|
+
const output = getFileContent(result.files, 'data/test/function/main.mcfunction');
|
|
97
|
+
expect(output).not.toContain('dead code');
|
|
98
|
+
expect(output).toContain('live code');
|
|
99
|
+
});
|
|
100
|
+
it('keeps decorated entry points', () => {
|
|
101
|
+
const source = `
|
|
102
|
+
@tick fn ticker() { }
|
|
103
|
+
@load fn loader() { }
|
|
104
|
+
@on(PlayerDeath) fn handler(player: Player) { say("event"); }
|
|
105
|
+
`;
|
|
106
|
+
const result = (0, index_1.compile)(source, { namespace: 'test' });
|
|
107
|
+
const names = result.ast.declarations.map(fn => fn.name);
|
|
108
|
+
expect(names).toContain('ticker');
|
|
109
|
+
expect(names).toContain('loader');
|
|
110
|
+
expect(names).toContain('handler');
|
|
111
|
+
});
|
|
112
|
+
it('can disable AST DCE through the compile API', () => {
|
|
113
|
+
const source = `
|
|
114
|
+
fn unused() { say("never called"); }
|
|
115
|
+
@tick fn main() { say("live"); }
|
|
116
|
+
`;
|
|
117
|
+
const result = (0, index_1.compile)(source, { namespace: 'test', dce: false });
|
|
118
|
+
expect(result.ast.declarations.map(fn => fn.name)).toEqual(['unused', 'main']);
|
|
119
|
+
expect(result.ir.functions.some(fn => fn.name === 'unused')).toBe(true);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
describe('CLI --no-dce', () => {
|
|
123
|
+
it('preserves unused functions when requested', () => {
|
|
124
|
+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'redscript-dce-cli-'));
|
|
125
|
+
const inputPath = path.join(tempDir, 'main.mcrs');
|
|
126
|
+
const outputDir = path.join(tempDir, 'out');
|
|
127
|
+
fs.writeFileSync(inputPath, [
|
|
128
|
+
'fn unused() { say("keep me"); }',
|
|
129
|
+
'@tick fn main() { say("live"); }',
|
|
130
|
+
'',
|
|
131
|
+
].join('\n'));
|
|
132
|
+
(0, child_process_1.execFileSync)(process.execPath, ['-r', 'ts-node/register', 'src/cli.ts', 'compile', inputPath, '-o', outputDir, '--namespace', 'test', '--no-dce'], { cwd: path.resolve(process.cwd()) });
|
|
133
|
+
const unusedPath = path.join(outputDir, 'data', 'test', 'function', 'unused.mcfunction');
|
|
134
|
+
expect(fs.existsSync(unusedPath)).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
//# sourceMappingURL=dce.test.js.map
|