redscript-mc 1.2.0 → 1.2.2

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 (55) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/README.md +53 -10
  3. package/README.zh.md +53 -10
  4. package/dist/__tests__/dce.test.d.ts +1 -0
  5. package/dist/__tests__/dce.test.js +137 -0
  6. package/dist/__tests__/lexer.test.js +19 -2
  7. package/dist/__tests__/lowering.test.js +8 -0
  8. package/dist/__tests__/mc-syntax.test.js +12 -0
  9. package/dist/__tests__/parser.test.js +10 -0
  10. package/dist/__tests__/runtime.test.js +13 -0
  11. package/dist/__tests__/typechecker.test.js +30 -0
  12. package/dist/ast/types.d.ts +22 -2
  13. package/dist/cli.js +15 -10
  14. package/dist/codegen/structure/index.d.ts +4 -1
  15. package/dist/codegen/structure/index.js +4 -2
  16. package/dist/compile.d.ts +1 -0
  17. package/dist/compile.js +4 -1
  18. package/dist/index.d.ts +1 -0
  19. package/dist/index.js +4 -1
  20. package/dist/lexer/index.d.ts +2 -1
  21. package/dist/lexer/index.js +89 -1
  22. package/dist/lowering/index.js +37 -1
  23. package/dist/optimizer/dce.d.ts +23 -0
  24. package/dist/optimizer/dce.js +592 -0
  25. package/dist/parser/index.d.ts +2 -0
  26. package/dist/parser/index.js +81 -16
  27. package/dist/typechecker/index.d.ts +2 -0
  28. package/dist/typechecker/index.js +49 -0
  29. package/docs/ARCHITECTURE.zh.md +1088 -0
  30. package/editors/vscode/.vscodeignore +3 -0
  31. package/editors/vscode/icon.png +0 -0
  32. package/editors/vscode/out/extension.js +834 -19
  33. package/editors/vscode/package-lock.json +2 -2
  34. package/editors/vscode/package.json +1 -1
  35. package/editors/vscode/syntaxes/redscript.tmLanguage.json +6 -2
  36. package/examples/spiral.mcrs +41 -0
  37. package/logo.png +0 -0
  38. package/package.json +1 -1
  39. package/src/__tests__/dce.test.ts +129 -0
  40. package/src/__tests__/lexer.test.ts +21 -2
  41. package/src/__tests__/lowering.test.ts +9 -0
  42. package/src/__tests__/mc-syntax.test.ts +14 -0
  43. package/src/__tests__/parser.test.ts +11 -0
  44. package/src/__tests__/runtime.test.ts +16 -0
  45. package/src/__tests__/typechecker.test.ts +33 -0
  46. package/src/ast/types.ts +14 -1
  47. package/src/cli.ts +24 -10
  48. package/src/codegen/structure/index.ts +13 -2
  49. package/src/compile.ts +5 -1
  50. package/src/index.ts +5 -1
  51. package/src/lexer/index.ts +102 -1
  52. package/src/lowering/index.ts +38 -2
  53. package/src/optimizer/dce.ts +619 -0
  54. package/src/parser/index.ts +97 -17
  55. package/src/typechecker/index.ts +65 -0
package/CHANGELOG.md CHANGED
@@ -8,18 +8,23 @@ All notable changes to RedScript will be documented in this file.
8
8
  - `is` type narrowing for entity checks (`if (e is Player)`)
9
9
  - `impl` blocks for struct methods
10
10
  - Static method calls (`Type::method()`)
11
+ - Runtime f-strings for output functions
11
12
  - Timer OOP API in stdlib
12
13
  - `setTimeout(delay, callback)` builtin
13
14
  - `setInterval(delay, callback)` builtin
14
15
  - `clearInterval(id)` builtin
15
16
  - `@on(Event)` static event system
16
17
  - PlayerDeath, PlayerJoin, BlockBreak, EntityKill, ItemUse
18
+ - Dead code elimination optimizer pass
17
19
  - Automatic namespace prefixing for scoreboard objectives
18
20
  - Comprehensive MC tag constants (313 tags)
19
21
 
20
22
  ### Changed
21
23
  - Stdlib timer functions now use OOP API
22
24
 
25
+ ### Documentation
26
+ - Updated README and docs site for the v1.2 language, stdlib, and builtins changes
27
+
23
28
  ## [1.1.0] - 2026-03-12
24
29
 
25
30
  ### Language Features
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)
@@ -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
@@ -78,6 +78,13 @@ describe('Lexer', () => {
78
78
  ['eof', ''],
79
79
  ]);
80
80
  });
81
+ it('tokenizes f-strings as a dedicated token', () => {
82
+ const tokens = tokenize('f"Hello {name}!"');
83
+ expect(tokens.map(t => [t.kind, t.value])).toEqual([
84
+ ['f_string', 'Hello {name}!'],
85
+ ['eof', ''],
86
+ ]);
87
+ });
81
88
  it('tokenizes byte literals (b suffix)', () => {
82
89
  const tokens = tokenize('20b 0B 127b');
83
90
  expect(tokens.map(t => [t.kind, t.value])).toEqual([
@@ -152,8 +159,18 @@ describe('Lexer', () => {
152
159
  });
153
160
  describe('operators', () => {
154
161
  it('tokenizes arithmetic operators', () => {
155
- const tokens = tokenize('+ - * / % ~ ^');
156
- expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', '~', '^', 'eof']);
162
+ const tokens = tokenize('+ - * / %');
163
+ expect(kinds(tokens)).toEqual(['+', '-', '*', '/', '%', 'eof']);
164
+ });
165
+ it('tokenizes relative and local coordinates', () => {
166
+ const tokens = tokenize('~ ~5 ~-3 ^ ^10 ^-2');
167
+ expect(kinds(tokens)).toEqual(['rel_coord', 'rel_coord', 'rel_coord', 'local_coord', 'local_coord', 'local_coord', 'eof']);
168
+ expect(tokens[0].value).toBe('~');
169
+ expect(tokens[1].value).toBe('~5');
170
+ expect(tokens[2].value).toBe('~-3');
171
+ expect(tokens[3].value).toBe('^');
172
+ expect(tokens[4].value).toBe('^10');
173
+ expect(tokens[5].value).toBe('^-2');
157
174
  });
158
175
  it('tokenizes comparison operators', () => {
159
176
  const tokens = tokenize('== != < <= > >=');
@@ -498,6 +498,14 @@ fn choose(dir: Direction) {
498
498
  const rawCmds = getRawCommands(fn);
499
499
  expect(rawCmds).toContain('tellraw @a ["",{"text":"You have "},{"score":{"name":"$score","objective":"rs"}},{"text":" points"}]');
500
500
  });
501
+ it('lowers f-string output builtins to tellraw/title JSON components', () => {
502
+ const ir = compile('fn test() { let score: int = 7; say(f"Score: {score}"); tellraw(@a, f"Score: {score}"); actionbar(@s, f"Score: {score}"); title(@s, f"Score: {score}"); }');
503
+ const fn = getFunction(ir, 'test');
504
+ const rawCmds = getRawCommands(fn);
505
+ expect(rawCmds).toContain('tellraw @a ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]');
506
+ expect(rawCmds).toContain('title @s actionbar ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]');
507
+ expect(rawCmds).toContain('title @s title ["",{"text":"Score: "},{"score":{"name":"$score","objective":"rs"}}]');
508
+ });
501
509
  it('lowers summon()', () => {
502
510
  const ir = compile('fn test() { summon("zombie"); }');
503
511
  const fn = getFunction(ir, 'test');
@@ -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() {
@@ -429,6 +429,16 @@ impl Point {
429
429
  ],
430
430
  });
431
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
+ });
432
442
  it('parses boolean literals', () => {
433
443
  expect(parseExpr('true')).toEqual({ kind: 'bool_lit', value: true });
434
444
  expect(parseExpr('false')).toEqual({ kind: 'bool_lit', value: false });
@@ -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');
@@ -110,6 +110,36 @@ fn test() {
110
110
  `);
111
111
  expect(errors).toHaveLength(0);
112
112
  });
113
+ it('allows f-strings in runtime output builtins', () => {
114
+ const errors = typeCheck(`
115
+ fn test() {
116
+ let score: int = 5;
117
+ say(f"Score: {score}");
118
+ tellraw(@a, f"Score: {score}");
119
+ actionbar(@s, f"Score: {score}");
120
+ title(@s, f"Score: {score}");
121
+ }
122
+ `);
123
+ expect(errors).toHaveLength(0);
124
+ });
125
+ it('rejects f-strings outside runtime output builtins', () => {
126
+ const errors = typeCheck(`
127
+ fn test() {
128
+ let msg: string = f"Score";
129
+ }
130
+ `);
131
+ expect(errors.length).toBeGreaterThan(0);
132
+ expect(errors[0].message).toContain('expected string, got format_string');
133
+ });
134
+ it('rejects unsupported f-string placeholder types', () => {
135
+ const errors = typeCheck(`
136
+ fn test() {
137
+ say(f"Flag: {true}");
138
+ }
139
+ `);
140
+ expect(errors.length).toBeGreaterThan(0);
141
+ expect(errors[0].message).toContain('f-string placeholder must be int or string');
142
+ });
113
143
  it('detects too many arguments', () => {
114
144
  const errors = typeCheck(`
115
145
  fn greet() {
@@ -11,7 +11,7 @@ export interface Span {
11
11
  endLine?: number;
12
12
  endCol?: number;
13
13
  }
14
- export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double';
14
+ export type PrimitiveType = 'int' | 'bool' | 'float' | 'string' | 'void' | 'BlockPos' | 'byte' | 'short' | 'long' | 'double' | 'format_string';
15
15
  export type EntityTypeName = 'entity' | 'Player' | 'Mob' | 'HostileMob' | 'PassiveMob' | 'Zombie' | 'Skeleton' | 'Creeper' | 'Spider' | 'Enderman' | 'Pig' | 'Cow' | 'Sheep' | 'Chicken' | 'Villager' | 'ArmorStand' | 'Item' | 'Arrow';
16
16
  export type TypeNode = {
17
17
  kind: 'named';
@@ -45,6 +45,18 @@ export interface LambdaExpr {
45
45
  returnType?: TypeNode;
46
46
  body: Expr | Block;
47
47
  }
48
+ export type FStringPart = {
49
+ kind: 'text';
50
+ value: string;
51
+ } | {
52
+ kind: 'expr';
53
+ expr: Expr;
54
+ };
55
+ export interface FStringExpr {
56
+ kind: 'f_string';
57
+ parts: FStringPart[];
58
+ span?: Span;
59
+ }
48
60
  export interface RangeExpr {
49
61
  min?: number;
50
62
  max?: number;
@@ -111,6 +123,14 @@ export type Expr = {
111
123
  kind: 'double_lit';
112
124
  value: number;
113
125
  span?: Span;
126
+ } | {
127
+ kind: 'rel_coord';
128
+ value: string;
129
+ span?: Span;
130
+ } | {
131
+ kind: 'local_coord';
132
+ value: string;
133
+ span?: Span;
114
134
  } | {
115
135
  kind: 'bool_lit';
116
136
  value: boolean;
@@ -127,7 +147,7 @@ export type Expr = {
127
147
  kind: 'str_interp';
128
148
  parts: Array<string | Expr>;
129
149
  span?: Span;
130
- } | {
150
+ } | FStringExpr | {
131
151
  kind: 'range_lit';
132
152
  range: RangeExpr;
133
153
  span?: Span;
package/dist/cli.js CHANGED
@@ -57,7 +57,7 @@ function printUsage() {
57
57
  RedScript Compiler
58
58
 
59
59
  Usage:
60
- redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>]
60
+ redscript compile <file> [-o <out>] [--output-nbt <file>] [--namespace <ns>] [--target <target>] [--no-dce]
61
61
  redscript watch <dir> [-o <outdir>] [--namespace <ns>] [--hot-reload <url>]
62
62
  redscript check <file>
63
63
  redscript fmt <file.mcrs> [file2.mcrs ...]
@@ -77,6 +77,7 @@ Options:
77
77
  --output-nbt <file> Output .nbt file path for structure target
78
78
  --namespace <ns> Datapack namespace (default: derived from filename)
79
79
  --target <target> Output target: datapack (default), cmdblock, or structure
80
+ --no-dce Disable AST dead code elimination
80
81
  --stats Print optimizer statistics
81
82
  --hot-reload <url> After each successful compile, POST to <url>/reload
82
83
  (use with redscript-testharness; e.g. http://localhost:25561)
@@ -99,7 +100,7 @@ function printVersion() {
99
100
  }
100
101
  }
101
102
  function parseArgs(args) {
102
- const result = {};
103
+ const result = { dce: true };
103
104
  let i = 0;
104
105
  while (i < args.length) {
105
106
  const arg = args[i];
@@ -127,6 +128,10 @@ function parseArgs(args) {
127
128
  result.stats = true;
128
129
  i++;
129
130
  }
131
+ else if (arg === '--no-dce') {
132
+ result.dce = false;
133
+ i++;
134
+ }
130
135
  else if (arg === '--hot-reload') {
131
136
  result.hotReload = args[++i];
132
137
  i++;
@@ -174,7 +179,7 @@ function printOptimizationStats(stats) {
174
179
  console.log(` constant folding: ${stats.constantFolds} constants folded`);
175
180
  console.log(` Total mcfunction commands: ${stats.totalCommandsBefore} -> ${stats.totalCommandsAfter} (${formatReduction(stats.totalCommandsBefore, stats.totalCommandsAfter)} reduction)`);
176
181
  }
177
- function compileCommand(file, output, namespace, target = 'datapack', showStats = false) {
182
+ function compileCommand(file, output, namespace, target = 'datapack', showStats = false, dce = true) {
178
183
  // Read source file
179
184
  if (!fs.existsSync(file)) {
180
185
  console.error(`Error: File not found: ${file}`);
@@ -183,7 +188,7 @@ function compileCommand(file, output, namespace, target = 'datapack', showStats
183
188
  const source = fs.readFileSync(file, 'utf-8');
184
189
  try {
185
190
  if (target === 'cmdblock') {
186
- const result = (0, index_1.compile)(source, { namespace, filePath: file });
191
+ const result = (0, index_1.compile)(source, { namespace, filePath: file, dce });
187
192
  printWarnings(result.warnings);
188
193
  // Generate command block JSON
189
194
  const hasTick = result.files.some(f => f.path.includes('__tick.mcfunction'));
@@ -201,7 +206,7 @@ function compileCommand(file, output, namespace, target = 'datapack', showStats
201
206
  }
202
207
  }
203
208
  else if (target === 'structure') {
204
- const structure = (0, structure_1.compileToStructure)(source, namespace, file);
209
+ const structure = (0, structure_1.compileToStructure)(source, namespace, file, { dce });
205
210
  fs.mkdirSync(path.dirname(output), { recursive: true });
206
211
  fs.writeFileSync(output, structure.buffer);
207
212
  console.log(`✓ Generated structure for ${file}`);
@@ -212,7 +217,7 @@ function compileCommand(file, output, namespace, target = 'datapack', showStats
212
217
  }
213
218
  }
214
219
  else {
215
- const result = (0, index_1.compile)(source, { namespace, filePath: file });
220
+ const result = (0, index_1.compile)(source, { namespace, filePath: file, dce });
216
221
  printWarnings(result.warnings);
217
222
  // Default: generate datapack
218
223
  // Create output directory
@@ -266,7 +271,7 @@ async function hotReload(url) {
266
271
  console.warn(`⚠ Hot reload failed (is the server running?): ${e.message}`);
267
272
  }
268
273
  }
269
- function watchCommand(dir, output, namespace, hotReloadUrl) {
274
+ function watchCommand(dir, output, namespace, hotReloadUrl, dce = true) {
270
275
  // Check if directory exists
271
276
  if (!fs.existsSync(dir)) {
272
277
  console.error(`Error: Directory not found: ${dir}`);
@@ -297,7 +302,7 @@ function watchCommand(dir, output, namespace, hotReloadUrl) {
297
302
  try {
298
303
  source = fs.readFileSync(file, 'utf-8');
299
304
  const ns = namespace ?? deriveNamespace(file);
300
- const result = (0, index_1.compile)(source, { namespace: ns, filePath: file });
305
+ const result = (0, index_1.compile)(source, { namespace: ns, filePath: file, dce });
301
306
  printWarnings(result.warnings);
302
307
  // Create output directory
303
308
  fs.mkdirSync(output, { recursive: true });
@@ -374,7 +379,7 @@ async function main() {
374
379
  const output = target === 'structure'
375
380
  ? (parsed.outputNbt ?? parsed.output ?? `./${namespace}.nbt`)
376
381
  : (parsed.output ?? './dist');
377
- compileCommand(parsed.file, output, namespace, target, parsed.stats);
382
+ compileCommand(parsed.file, output, namespace, target, parsed.stats, parsed.dce);
378
383
  }
379
384
  break;
380
385
  case 'watch':
@@ -383,7 +388,7 @@ async function main() {
383
388
  printUsage();
384
389
  process.exit(1);
385
390
  }
386
- watchCommand(parsed.file, parsed.output ?? './dist', parsed.namespace, parsed.hotReload);
391
+ watchCommand(parsed.file, parsed.output ?? './dist', parsed.namespace, parsed.hotReload, parsed.dce);
387
392
  break;
388
393
  case 'check':
389
394
  if (!parsed.file) {