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.
Files changed (83) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/cli.test.js +138 -0
  5. package/dist/__tests__/codegen.test.js +25 -0
  6. package/dist/__tests__/dce.test.d.ts +1 -0
  7. package/dist/__tests__/dce.test.js +137 -0
  8. package/dist/__tests__/e2e.test.js +190 -12
  9. package/dist/__tests__/lexer.test.js +31 -4
  10. package/dist/__tests__/lowering.test.js +172 -9
  11. package/dist/__tests__/mc-integration.test.js +145 -51
  12. package/dist/__tests__/mc-syntax.test.js +12 -0
  13. package/dist/__tests__/optimizer-advanced.test.js +3 -3
  14. package/dist/__tests__/parser.test.js +90 -0
  15. package/dist/__tests__/runtime.test.js +21 -8
  16. package/dist/__tests__/typechecker.test.js +188 -0
  17. package/dist/ast/types.d.ts +42 -3
  18. package/dist/cli.js +15 -10
  19. package/dist/codegen/mcfunction/index.js +30 -1
  20. package/dist/codegen/structure/index.d.ts +4 -1
  21. package/dist/codegen/structure/index.js +29 -2
  22. package/dist/compile.d.ts +11 -0
  23. package/dist/compile.js +40 -6
  24. package/dist/events/types.d.ts +35 -0
  25. package/dist/events/types.js +59 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +7 -3
  28. package/dist/ir/types.d.ts +4 -0
  29. package/dist/lexer/index.d.ts +2 -1
  30. package/dist/lexer/index.js +91 -1
  31. package/dist/lowering/index.d.ts +32 -1
  32. package/dist/lowering/index.js +476 -16
  33. package/dist/optimizer/dce.d.ts +23 -0
  34. package/dist/optimizer/dce.js +591 -0
  35. package/dist/parser/index.d.ts +4 -0
  36. package/dist/parser/index.js +160 -26
  37. package/dist/typechecker/index.d.ts +19 -0
  38. package/dist/typechecker/index.js +392 -17
  39. package/docs/ARCHITECTURE.zh.md +1088 -0
  40. package/docs/ENTITY_TYPE_SYSTEM.md +242 -0
  41. package/editors/vscode/.vscodeignore +3 -0
  42. package/editors/vscode/CHANGELOG.md +9 -0
  43. package/editors/vscode/icon.png +0 -0
  44. package/editors/vscode/out/extension.js +1144 -72
  45. package/editors/vscode/package-lock.json +2 -2
  46. package/editors/vscode/package.json +1 -1
  47. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  48. package/examples/spiral.mcrs +79 -0
  49. package/logo.png +0 -0
  50. package/package.json +1 -1
  51. package/src/__tests__/cli.test.ts +166 -0
  52. package/src/__tests__/codegen.test.ts +27 -0
  53. package/src/__tests__/dce.test.ts +129 -0
  54. package/src/__tests__/e2e.test.ts +201 -12
  55. package/src/__tests__/fixtures/event-test.mcrs +13 -0
  56. package/src/__tests__/fixtures/impl-test.mcrs +46 -0
  57. package/src/__tests__/fixtures/interval-test.mcrs +11 -0
  58. package/src/__tests__/fixtures/is-check-test.mcrs +20 -0
  59. package/src/__tests__/fixtures/timeout-test.mcrs +7 -0
  60. package/src/__tests__/lexer.test.ts +35 -4
  61. package/src/__tests__/lowering.test.ts +187 -9
  62. package/src/__tests__/mc-integration.test.ts +166 -51
  63. package/src/__tests__/mc-syntax.test.ts +14 -0
  64. package/src/__tests__/optimizer-advanced.test.ts +3 -3
  65. package/src/__tests__/parser.test.ts +102 -5
  66. package/src/__tests__/runtime.test.ts +24 -8
  67. package/src/__tests__/typechecker.test.ts +204 -0
  68. package/src/ast/types.ts +39 -2
  69. package/src/cli.ts +24 -10
  70. package/src/codegen/mcfunction/index.ts +31 -1
  71. package/src/codegen/structure/index.ts +40 -2
  72. package/src/compile.ts +59 -7
  73. package/src/events/types.ts +69 -0
  74. package/src/index.ts +9 -4
  75. package/src/ir/types.ts +4 -0
  76. package/src/lexer/index.ts +105 -2
  77. package/src/lowering/index.ts +566 -18
  78. package/src/optimizer/dce.ts +618 -0
  79. package/src/parser/index.ts +187 -29
  80. package/src/stdlib/README.md +34 -4
  81. package/src/stdlib/tags.mcrs +951 -0
  82. package/src/stdlib/timer.mcrs +54 -33
  83. 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="https://img.shields.io/badge/RedScript-1.0-red?style=for-the-badge&logo=minecraft&logoColor=white" alt="RedScript" />
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
  [![CI](https://github.com/bkmashiro/redscript/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bkmashiro/redscript/actions/workflows/ci.yml)
10
- [![Tests](https://img.shields.io/badge/tests-510%20passing-brightgreen)](https://github.com/bkmashiro/redscript)
12
+ [![Tests](https://img.shields.io/badge/tests-573%2B%20passing-brightgreen)](https://github.com/bkmashiro/redscript)
11
13
  [![npm](https://img.shields.io/npm/v/redscript-mc?color=cb3837)](https://www.npmjs.com/package/redscript-mc)
12
14
  [![npm downloads](https://img.shields.io/npm/dm/redscript-mc?color=cb3837)](https://www.npmjs.com/package/redscript-mc)
13
15
  [![VSCode](https://img.shields.io/badge/VSCode-Extension-007ACC?logo=visualstudiocode)](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="https://img.shields.io/badge/RedScript-1.0-red?style=for-the-badge&logo=minecraft&logoColor=white" alt="RedScript" />
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
  [![CI](https://github.com/bkmashiro/redscript/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/bkmashiro/redscript/actions/workflows/ci.yml)
10
- [![Tests](https://img.shields.io/badge/tests-510%20passing-brightgreen)](https://github.com/bkmashiro/redscript)
12
+ [![Tests](https://img.shields.io/badge/tests-573%2B%20passing-brightgreen)](https://github.com/bkmashiro/redscript)
11
13
  [![npm](https://img.shields.io/npm/v/redscript-mc?color=cb3837)](https://www.npmjs.com/package/redscript-mc)
12
14
  [![npm downloads](https://img.shields.io/npm/dm/redscript-mc?color=cb3837)](https://www.npmjs.com/package/redscript-mc)
13
15
  [![VSCode](https://img.shields.io/badge/VSCode-插件-007ACC?logo=visualstudiocode)](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