redscript-mc 1.2.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 +5 -0
- package/README.md +53 -10
- package/README.zh.md +53 -10
- package/dist/__tests__/dce.test.d.ts +1 -0
- package/dist/__tests__/dce.test.js +137 -0
- package/dist/__tests__/lexer.test.js +19 -2
- package/dist/__tests__/lowering.test.js +8 -0
- package/dist/__tests__/mc-syntax.test.js +12 -0
- package/dist/__tests__/parser.test.js +10 -0
- package/dist/__tests__/runtime.test.js +13 -0
- package/dist/__tests__/typechecker.test.js +30 -0
- package/dist/ast/types.d.ts +22 -2
- package/dist/cli.js +15 -10
- package/dist/codegen/structure/index.d.ts +4 -1
- package/dist/codegen/structure/index.js +4 -2
- package/dist/compile.d.ts +1 -0
- package/dist/compile.js +4 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +4 -1
- package/dist/lexer/index.d.ts +2 -1
- package/dist/lexer/index.js +89 -1
- package/dist/lowering/index.js +37 -1
- package/dist/optimizer/dce.d.ts +23 -0
- package/dist/optimizer/dce.js +591 -0
- package/dist/parser/index.d.ts +2 -0
- package/dist/parser/index.js +81 -16
- package/dist/typechecker/index.d.ts +2 -0
- package/dist/typechecker/index.js +49 -0
- package/docs/ARCHITECTURE.zh.md +1088 -0
- package/editors/vscode/.vscodeignore +3 -0
- package/editors/vscode/icon.png +0 -0
- 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__/dce.test.ts +129 -0
- package/src/__tests__/lexer.test.ts +21 -2
- package/src/__tests__/lowering.test.ts +9 -0
- package/src/__tests__/mc-syntax.test.ts +14 -0
- package/src/__tests__/parser.test.ts +11 -0
- package/src/__tests__/runtime.test.ts +16 -0
- package/src/__tests__/typechecker.test.ts +33 -0
- package/src/ast/types.ts +14 -1
- package/src/cli.ts +24 -10
- package/src/codegen/structure/index.ts +13 -2
- package/src/compile.ts +5 -1
- package/src/index.ts +5 -1
- package/src/lexer/index.ts +102 -1
- package/src/lowering/index.ts +38 -2
- package/src/optimizer/dce.ts +618 -0
- package/src/parser/index.ts +97 -17
- 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="
|
|
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)
|
|
@@ -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(['+', '-', '*', '/', '%', '
|
|
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() {
|
package/dist/ast/types.d.ts
CHANGED
|
@@ -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) {
|