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.
- 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 +592 -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/out/extension.js +834 -19
- 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 +41 -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 +619 -0
- package/src/parser/index.ts +97 -17
- package/src/typechecker/index.ts +65 -0
|
@@ -0,0 +1,1088 @@
|
|
|
1
|
+
# RedScript 编译器架构
|
|
2
|
+
|
|
3
|
+
这份文档用最简单的语言,解释 RedScript 编译器怎样把 `.mcrs` 源码变成 Minecraft datapack 里的 `.mcfunction` 文件。
|
|
4
|
+
|
|
5
|
+
你可以把它理解成一条流水线:
|
|
6
|
+
|
|
7
|
+
- `Lexer` 把字符串切成一个个小块
|
|
8
|
+
- `Parser` 把小块拼成语法树
|
|
9
|
+
- `TypeChecker` 检查类型和上下文
|
|
10
|
+
- `Lowering` 把高级语法翻成更容易生成命令的 IR
|
|
11
|
+
- `CodeGen` 把 IR 输出成 datapack 文件
|
|
12
|
+
- `Optimizer` 在中间或最后阶段删掉浪费的内容
|
|
13
|
+
|
|
14
|
+
## 编译流程总览
|
|
15
|
+
|
|
16
|
+
源码 → Lexer → Parser → TypeChecker → Lowering → CodeGen → .mcfunction
|
|
17
|
+
|
|
18
|
+
```text
|
|
19
|
+
+-------------------+
|
|
20
|
+
| RedScript 源码 |
|
|
21
|
+
+---------+---------+
|
|
22
|
+
|
|
|
23
|
+
v
|
|
24
|
+
+-------------------+
|
|
25
|
+
| Lexer |
|
|
26
|
+
| 字符 -> Token |
|
|
27
|
+
+---------+---------+
|
|
28
|
+
|
|
|
29
|
+
v
|
|
30
|
+
+-------------------+
|
|
31
|
+
| Parser |
|
|
32
|
+
| Token -> AST |
|
|
33
|
+
+---------+---------+
|
|
34
|
+
|
|
|
35
|
+
v
|
|
36
|
+
+-------------------+
|
|
37
|
+
| TypeChecker |
|
|
38
|
+
| AST + 符号表 |
|
|
39
|
+
| + @s 上下文 |
|
|
40
|
+
+---------+---------+
|
|
41
|
+
|
|
|
42
|
+
v
|
|
43
|
+
+-------------------+
|
|
44
|
+
| Lowering |
|
|
45
|
+
| AST -> IR |
|
|
46
|
+
| 变量 -> scoreboard|
|
|
47
|
+
+---------+---------+
|
|
48
|
+
|
|
|
49
|
+
v
|
|
50
|
+
+-------------------+
|
|
51
|
+
| Optimizer |
|
|
52
|
+
| DCE / 命令合并 |
|
|
53
|
+
+---------+---------+
|
|
54
|
+
|
|
|
55
|
+
v
|
|
56
|
+
+-------------------+
|
|
57
|
+
| CodeGen |
|
|
58
|
+
| IR -> mcfunction |
|
|
59
|
+
| + tick/load tags |
|
|
60
|
+
+---------+---------+
|
|
61
|
+
|
|
|
62
|
+
v
|
|
63
|
+
+-------------------+
|
|
64
|
+
| datapack 输出 |
|
|
65
|
+
| .mcfunction/json |
|
|
66
|
+
+-------------------+
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 1. 词法分析 (Lexer)
|
|
70
|
+
|
|
71
|
+
### 干什么的?
|
|
72
|
+
|
|
73
|
+
Lexer 的工作很简单:从左到右读源码,把字符流切成一个个 `Token`。
|
|
74
|
+
|
|
75
|
+
比如这段代码:
|
|
76
|
+
|
|
77
|
+
```rs
|
|
78
|
+
let hp: int = scoreboard_get(@s, "health");
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
会大致切成:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
let | hp | : | int | = | scoreboard_get | ( | @s | , | "health" | ) | ;
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
这样后面的 Parser 就不用再关心“字符”了,只需要处理“词”。
|
|
88
|
+
|
|
89
|
+
### 核心数据结构 Token
|
|
90
|
+
|
|
91
|
+
简化后的核心结构就是:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
type Token = {
|
|
95
|
+
kind: TokenKind
|
|
96
|
+
value: string
|
|
97
|
+
line: number
|
|
98
|
+
col: number
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
这里最重要的是两件事:
|
|
103
|
+
|
|
104
|
+
- `kind`:这个词是什么,比如 `ident`、`int_lit`、`selector`
|
|
105
|
+
- `line/col`:出错时要告诉用户“哪一行哪一列有问题”
|
|
106
|
+
|
|
107
|
+
### 实际实现里要特别处理什么?
|
|
108
|
+
|
|
109
|
+
RedScript 的 Lexer 不只是普通编程语言的 Lexer,它还要处理 Minecraft 特有语法。
|
|
110
|
+
|
|
111
|
+
#### 1. `@a` 和 `@tick` 不是一回事
|
|
112
|
+
|
|
113
|
+
- `@a`、`@e[...]`、`@s` 是实体选择器,记成 `selector`
|
|
114
|
+
- `@tick`、`@on(PlayerDeath)`、`@on_trigger("x")` 是装饰器,记成 `decorator`
|
|
115
|
+
|
|
116
|
+
也就是说,Lexer 在看到 `@` 时,要先判断后面跟的是选择器还是装饰器。
|
|
117
|
+
|
|
118
|
+
#### 2. `@e[type=zombie,tag=boss]` 会尽量作为一个整体 Token
|
|
119
|
+
|
|
120
|
+
因为中括号里可能还带 NBT、范围、逗号,先整体收起来更稳。等到 Parser 再拆内部字段。
|
|
121
|
+
|
|
122
|
+
#### 3. 范围字面量 `1..10`
|
|
123
|
+
|
|
124
|
+
RedScript 支持:
|
|
125
|
+
|
|
126
|
+
- `..5`
|
|
127
|
+
- `1..`
|
|
128
|
+
- `1..10`
|
|
129
|
+
|
|
130
|
+
它们都不是两个点,而是一个完整的 `range_lit`。
|
|
131
|
+
|
|
132
|
+
#### 4. `->`、`==`、`+=` 这些双字符运算符
|
|
133
|
+
|
|
134
|
+
如果 Lexer 先把 `->` 切成 `-` 和 `>`,Parser 就会很难处理。
|
|
135
|
+
|
|
136
|
+
#### 5. f-string
|
|
137
|
+
|
|
138
|
+
`f"hello {name}"` 不会被当成普通字符串,而是单独记成 `f_string`,后面会编译成 `tellraw` JSON。
|
|
139
|
+
|
|
140
|
+
### 简化实现(10 行伪代码)
|
|
141
|
+
|
|
142
|
+
```text
|
|
143
|
+
while not end:
|
|
144
|
+
ch = advance()
|
|
145
|
+
if ch is whitespace: continue
|
|
146
|
+
if startsWith("//"): skipLine()
|
|
147
|
+
else if ch begins number: readNumberOrRange()
|
|
148
|
+
else if ch == '@': readSelectorOrDecorator()
|
|
149
|
+
else if ch begins letter: readIdentifierOrKeyword()
|
|
150
|
+
else if ch == '"': readString()
|
|
151
|
+
else if ch begins operator: readOperator()
|
|
152
|
+
else if ch begins delimiter: addToken()
|
|
153
|
+
else: reportError()
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 一个简化示例
|
|
157
|
+
|
|
158
|
+
输入:
|
|
159
|
+
|
|
160
|
+
```rs
|
|
161
|
+
@on(PlayerDeath)
|
|
162
|
+
fn handle() { tellraw(@a, f"HP: {scoreboard_get(@s, "hp")}"); }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
输出:
|
|
166
|
+
|
|
167
|
+
```text
|
|
168
|
+
decorator("@on(PlayerDeath)")
|
|
169
|
+
fn
|
|
170
|
+
ident("handle")
|
|
171
|
+
(
|
|
172
|
+
)
|
|
173
|
+
{
|
|
174
|
+
ident("tellraw")
|
|
175
|
+
(
|
|
176
|
+
selector("@a")
|
|
177
|
+
,
|
|
178
|
+
f_string("HP: { ... }")
|
|
179
|
+
)
|
|
180
|
+
;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
## 2. 语法分析 (Parser)
|
|
185
|
+
|
|
186
|
+
### 干什么的?
|
|
187
|
+
|
|
188
|
+
Parser 把 Token 变成 AST。
|
|
189
|
+
|
|
190
|
+
AST 的意思是“抽象语法树”。它不关心空格和换行,只关心代码的结构。
|
|
191
|
+
|
|
192
|
+
比如:
|
|
193
|
+
|
|
194
|
+
```rs
|
|
195
|
+
let x: int = 1 + 2 * 3;
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
真正重要的不是字符顺序,而是:
|
|
199
|
+
|
|
200
|
+
- 这是一个 `let` 语句
|
|
201
|
+
- 变量名叫 `x`
|
|
202
|
+
- 类型是 `int`
|
|
203
|
+
- 初始化表达式是 `1 + (2 * 3)`
|
|
204
|
+
|
|
205
|
+
### 核心数据结构 AST
|
|
206
|
+
|
|
207
|
+
可以把 AST 想成一棵树:
|
|
208
|
+
|
|
209
|
+
```text
|
|
210
|
+
let
|
|
211
|
+
├─ name: x
|
|
212
|
+
├─ type: int
|
|
213
|
+
└─ init: binary(+)
|
|
214
|
+
├─ left: 1
|
|
215
|
+
└─ right: binary(*)
|
|
216
|
+
├─ left: 2
|
|
217
|
+
└─ right: 3
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
项目里的 AST 结构比这个更丰富,除了函数、变量、表达式,还包含:
|
|
221
|
+
|
|
222
|
+
- `struct`
|
|
223
|
+
- `impl`
|
|
224
|
+
- `enum`
|
|
225
|
+
- `foreach`
|
|
226
|
+
- `execute`
|
|
227
|
+
- `f_string`
|
|
228
|
+
- `static_call`
|
|
229
|
+
- `member_assign`
|
|
230
|
+
|
|
231
|
+
### 递归下降解析(简化示例)
|
|
232
|
+
|
|
233
|
+
RedScript 的 Parser 用的是“递归下降”。
|
|
234
|
+
|
|
235
|
+
意思是:每种语法都写一个函数。
|
|
236
|
+
|
|
237
|
+
例如:
|
|
238
|
+
|
|
239
|
+
- `parseFnDecl()` 解析函数
|
|
240
|
+
- `parseStmt()` 解析语句
|
|
241
|
+
- `parseExpr()` 解析表达式
|
|
242
|
+
- `parseSelector()` 解析实体选择器
|
|
243
|
+
|
|
244
|
+
最常见的写法是:
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
parseStmt():
|
|
248
|
+
if current is let: return parseLet()
|
|
249
|
+
if current is if: return parseIf()
|
|
250
|
+
if current is while: return parseWhile()
|
|
251
|
+
if current is foreach: return parseForeach()
|
|
252
|
+
return parseExprStmt()
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### 表达式优先级怎么做?
|
|
256
|
+
|
|
257
|
+
表达式不是按看到的顺序直接拼,而是按优先级解析。
|
|
258
|
+
|
|
259
|
+
例如:
|
|
260
|
+
|
|
261
|
+
```rs
|
|
262
|
+
1 + 2 * 3
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
必须解析成:
|
|
266
|
+
|
|
267
|
+
```text
|
|
268
|
+
(+)
|
|
269
|
+
/ \
|
|
270
|
+
1 (*)
|
|
271
|
+
/ \
|
|
272
|
+
2 3
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
项目里的实现使用 precedence climbing。思路是:
|
|
276
|
+
|
|
277
|
+
```text
|
|
278
|
+
parseBinaryExpr(minPrec):
|
|
279
|
+
left = parseUnary()
|
|
280
|
+
while current token is binary op and prec >= minPrec:
|
|
281
|
+
op = advance()
|
|
282
|
+
right = parseBinaryExpr(prec(op) + 1)
|
|
283
|
+
left = Binary(op, left, right)
|
|
284
|
+
return left
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 选择器怎么解析?
|
|
288
|
+
|
|
289
|
+
Lexer 已经把 `@e[type=zombie, distance=..5]` 收成一个 token。
|
|
290
|
+
|
|
291
|
+
Parser 再做第二次拆分:
|
|
292
|
+
|
|
293
|
+
```text
|
|
294
|
+
selector token
|
|
295
|
+
|
|
|
296
|
+
+--> kind = @e
|
|
297
|
+
+--> filters.type = zombie
|
|
298
|
+
+--> filters.distance = { max: 5 }
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
简化伪代码:
|
|
302
|
+
|
|
303
|
+
```text
|
|
304
|
+
parseSelector(raw):
|
|
305
|
+
split "@e[...]" into kind + filterString
|
|
306
|
+
for each key=value in filterString:
|
|
307
|
+
if key == type: save string
|
|
308
|
+
if key == distance: parseRange()
|
|
309
|
+
if key == tag: push to tag/notTag
|
|
310
|
+
if key == limit: parse int
|
|
311
|
+
return EntitySelector
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### `impl` 的语法糖
|
|
315
|
+
|
|
316
|
+
Parser 在解析 `impl PlayerState { fn heal(self, x: int) {} }` 时,会把它保存成 `ImplBlock`。
|
|
317
|
+
|
|
318
|
+
这里有个小技巧:
|
|
319
|
+
|
|
320
|
+
- 如果方法第一个参数叫 `self`
|
|
321
|
+
- 并且没有显式写类型
|
|
322
|
+
|
|
323
|
+
Parser 会自动把它补成当前 `impl` 的结构体类型。
|
|
324
|
+
|
|
325
|
+
也就是:
|
|
326
|
+
|
|
327
|
+
```rs
|
|
328
|
+
impl Counter {
|
|
329
|
+
fn inc(self) {}
|
|
330
|
+
}
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
会被理解成类似:
|
|
334
|
+
|
|
335
|
+
```rs
|
|
336
|
+
fn inc(self: Counter) {}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
## 3. 类型检查 (TypeChecker)
|
|
340
|
+
|
|
341
|
+
### 干什么的?
|
|
342
|
+
|
|
343
|
+
Parser 只保证“语法长得像代码”。
|
|
344
|
+
|
|
345
|
+
TypeChecker 负责保证“语义讲得通”。
|
|
346
|
+
|
|
347
|
+
例如:
|
|
348
|
+
|
|
349
|
+
- `int` 不能直接赋值给 `string`
|
|
350
|
+
- 没声明的变量不能用
|
|
351
|
+
- 返回值类型要对得上
|
|
352
|
+
- `@on(PlayerDeath)` 的函数参数必须符合事件要求
|
|
353
|
+
- `self` 必须出现在方法的第一个参数位置
|
|
354
|
+
|
|
355
|
+
### 符号表
|
|
356
|
+
|
|
357
|
+
TypeChecker 里最重要的数据结构之一是符号表。它本质上是名字到类型的映射。
|
|
358
|
+
|
|
359
|
+
```text
|
|
360
|
+
scope
|
|
361
|
+
├─ x -> int
|
|
362
|
+
├─ hp -> int
|
|
363
|
+
├─ player -> Player
|
|
364
|
+
└─ state -> struct Counter
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
项目里实际上维护了几张表:
|
|
368
|
+
|
|
369
|
+
- `functions`: 普通函数表
|
|
370
|
+
- `implMethods`: `类型 -> 方法名 -> FnDecl`
|
|
371
|
+
- `structs`: 结构体字段表
|
|
372
|
+
- `enums`: 枚举表
|
|
373
|
+
- `consts`: 常量表
|
|
374
|
+
- `scope`: 当前局部作用域
|
|
375
|
+
|
|
376
|
+
### 类型推断流程
|
|
377
|
+
|
|
378
|
+
它的工作顺序可以理解成两遍:
|
|
379
|
+
|
|
380
|
+
```text
|
|
381
|
+
第一遍:
|
|
382
|
+
收集函数、struct、enum、impl 方法签名
|
|
383
|
+
|
|
384
|
+
第二遍:
|
|
385
|
+
逐个检查函数体里的语句和表达式
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
这样做的好处是:后面调用前面或后面的函数都能识别。
|
|
389
|
+
|
|
390
|
+
简化伪代码:
|
|
391
|
+
|
|
392
|
+
```text
|
|
393
|
+
check(program):
|
|
394
|
+
collect function signatures
|
|
395
|
+
collect impl methods
|
|
396
|
+
collect struct fields
|
|
397
|
+
collect enums and consts
|
|
398
|
+
for each function:
|
|
399
|
+
checkFunctionBody()
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### `@s` 上下文追踪
|
|
403
|
+
|
|
404
|
+
这是 RedScript 里一个很关键,也很容易忽略的点。
|
|
405
|
+
|
|
406
|
+
在 Minecraft 里,`@s` 的真实类型不是固定的,它要看当前执行上下文。
|
|
407
|
+
|
|
408
|
+
例如:
|
|
409
|
+
|
|
410
|
+
```rs
|
|
411
|
+
foreach (z in @e[type=zombie]) {
|
|
412
|
+
// 这里的 @s 实际上就是 zombie
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
或者:
|
|
417
|
+
|
|
418
|
+
```rs
|
|
419
|
+
as @a {
|
|
420
|
+
// 这里的 @s 是 Player
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
TypeChecker 的做法是维护一个 `selfTypeStack`:
|
|
425
|
+
|
|
426
|
+
```text
|
|
427
|
+
初始: [entity]
|
|
428
|
+
|
|
429
|
+
进入 as @a:
|
|
430
|
+
push(Player)
|
|
431
|
+
|
|
432
|
+
进入 foreach @e[type=zombie]:
|
|
433
|
+
push(Zombie)
|
|
434
|
+
|
|
435
|
+
离开作用域:
|
|
436
|
+
pop()
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
所以当类型系统看到 `@s` 时,不是简单返回 `entity`,而是返回“栈顶当前类型”。
|
|
440
|
+
|
|
441
|
+
ASCII 图:
|
|
442
|
+
|
|
443
|
+
```text
|
|
444
|
+
外层函数
|
|
445
|
+
selfTypeStack = [entity]
|
|
446
|
+
|
|
447
|
+
as @a {
|
|
448
|
+
selfTypeStack = [entity, Player]
|
|
449
|
+
|
|
450
|
+
foreach mob in @e[type=zombie] {
|
|
451
|
+
selfTypeStack = [entity, Player, Zombie]
|
|
452
|
+
@s ==> Zombie
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
离开 foreach
|
|
456
|
+
selfTypeStack = [entity, Player]
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
离开 as
|
|
460
|
+
selfTypeStack = [entity]
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### 一个简化示例
|
|
464
|
+
|
|
465
|
+
```rs
|
|
466
|
+
@on(PlayerDeath)
|
|
467
|
+
fn on_die(player: Player) {
|
|
468
|
+
let x: int = 1;
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
TypeChecker 会检查:
|
|
473
|
+
|
|
474
|
+
- `PlayerDeath` 是不是合法事件
|
|
475
|
+
- 这个事件是否要求 1 个参数
|
|
476
|
+
- 参数类型是不是 `Player`
|
|
477
|
+
|
|
478
|
+
如果写成:
|
|
479
|
+
|
|
480
|
+
```rs
|
|
481
|
+
@on(PlayerDeath)
|
|
482
|
+
fn on_die(x: int) {}
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
就会报错。
|
|
486
|
+
|
|
487
|
+
## 4. IR 降级 (Lowering)
|
|
488
|
+
|
|
489
|
+
### 干什么的?
|
|
490
|
+
|
|
491
|
+
Parser 和 TypeChecker 处理的是“源码世界”的结构。
|
|
492
|
+
|
|
493
|
+
但是生成 Minecraft 命令时,直接面对 AST 很麻烦。因为 Minecraft 命令本质上更接近:
|
|
494
|
+
|
|
495
|
+
- 赋值
|
|
496
|
+
- 比较
|
|
497
|
+
- 跳转
|
|
498
|
+
- 调函数
|
|
499
|
+
- 原始命令
|
|
500
|
+
|
|
501
|
+
所以 Lowering 会把 AST 变成更朴素的 IR。
|
|
502
|
+
|
|
503
|
+
项目里的 IR 很像三地址码(TAC):
|
|
504
|
+
|
|
505
|
+
- `assign`
|
|
506
|
+
- `binop`
|
|
507
|
+
- `cmp`
|
|
508
|
+
- `jump`
|
|
509
|
+
- `jump_if`
|
|
510
|
+
- `call`
|
|
511
|
+
- `return`
|
|
512
|
+
- `raw`
|
|
513
|
+
|
|
514
|
+
### 高级语法 → 低级 IR
|
|
515
|
+
|
|
516
|
+
举个例子:
|
|
517
|
+
|
|
518
|
+
```rs
|
|
519
|
+
let c: int = a + b;
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
会变成类似:
|
|
523
|
+
|
|
524
|
+
```text
|
|
525
|
+
t0 = a
|
|
526
|
+
t0 += b
|
|
527
|
+
c = t0
|
|
528
|
+
```
|
|
529
|
+
|
|
530
|
+
再比如:
|
|
531
|
+
|
|
532
|
+
```rs
|
|
533
|
+
if (hp > 0) { heal(); } else { die(); }
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
会变成:
|
|
537
|
+
|
|
538
|
+
```text
|
|
539
|
+
t0 = (hp > 0)
|
|
540
|
+
if t0 goto then
|
|
541
|
+
goto else
|
|
542
|
+
|
|
543
|
+
then:
|
|
544
|
+
call heal
|
|
545
|
+
goto end
|
|
546
|
+
|
|
547
|
+
else:
|
|
548
|
+
call die
|
|
549
|
+
goto end
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
ASCII 图:
|
|
553
|
+
|
|
554
|
+
```text
|
|
555
|
+
AST if/while/foreach
|
|
556
|
+
|
|
|
557
|
+
v
|
|
558
|
+
+------------------+
|
|
559
|
+
| Lowering |
|
|
560
|
+
| 拆成基本块和跳转 |
|
|
561
|
+
+--------+---------+
|
|
562
|
+
|
|
|
563
|
+
v
|
|
564
|
+
entry -> cmp -> jump_if
|
|
565
|
+
| |
|
|
566
|
+
v v
|
|
567
|
+
then else
|
|
568
|
+
\ /
|
|
569
|
+
\ /
|
|
570
|
+
-> end
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
### scoreboard 变量分配
|
|
574
|
+
|
|
575
|
+
Minecraft 没有真正的本地变量,所以编译器要自己模拟。
|
|
576
|
+
|
|
577
|
+
RedScript 的主要做法是:
|
|
578
|
+
|
|
579
|
+
- 整数变量放进 scoreboard objective `rs`
|
|
580
|
+
- 变量名映射成 fake player,比如 `$hp`
|
|
581
|
+
- 临时变量映射成 `$_0`、`$_1`
|
|
582
|
+
- 返回值放进 `$ret`
|
|
583
|
+
- 参数通过 `$p0`、`$p1` 传递
|
|
584
|
+
|
|
585
|
+
也就是:
|
|
586
|
+
|
|
587
|
+
```text
|
|
588
|
+
源码变量 x
|
|
589
|
+
|
|
|
590
|
+
v
|
|
591
|
+
scoreboard players set $x rs 0
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
IR 设计图:
|
|
595
|
+
|
|
596
|
+
```text
|
|
597
|
+
RedScript 变量
|
|
598
|
+
|
|
|
599
|
+
+--> 局部变量 -> $name on objective rs
|
|
600
|
+
+--> 临时变量 -> $_N on objective rs
|
|
601
|
+
+--> 返回值 -> $ret on objective rs
|
|
602
|
+
+--> 参数槽位 -> $p0, $p1, ...
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
这个设计很朴素,但非常适合 Minecraft 的执行模型。
|
|
606
|
+
|
|
607
|
+
### 函数调用约定
|
|
608
|
+
|
|
609
|
+
RedScript 的函数调用不是靠真正的调用栈,而是靠固定约定:
|
|
610
|
+
|
|
611
|
+
1. 调用方先把参数写到 `$p0`, `$p1`, ...`
|
|
612
|
+
2. 执行 `function namespace:fn_name`
|
|
613
|
+
3. 被调函数入口把 `$pN` 复制到自己的局部变量
|
|
614
|
+
4. 返回值写入 `$ret`
|
|
615
|
+
5. 调用方再把 `$ret` 复制出来
|
|
616
|
+
|
|
617
|
+
简化示例:
|
|
618
|
+
|
|
619
|
+
```text
|
|
620
|
+
caller:
|
|
621
|
+
$p0 = a
|
|
622
|
+
$p1 = b
|
|
623
|
+
function demo:add
|
|
624
|
+
x = $ret
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### `foreach` 怎么降级?
|
|
628
|
+
|
|
629
|
+
这是一个很典型的“高级语法拆低级语法”案例。
|
|
630
|
+
|
|
631
|
+
源码:
|
|
632
|
+
|
|
633
|
+
```rs
|
|
634
|
+
foreach (z in @e[type=zombie]) {
|
|
635
|
+
damage(z, 1);
|
|
636
|
+
}
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
Lowering 的做法不是在当前位置展开所有逻辑,而是:
|
|
640
|
+
|
|
641
|
+
1. 生成一个子函数
|
|
642
|
+
2. 把循环体放进子函数
|
|
643
|
+
3. 主函数发出:
|
|
644
|
+
|
|
645
|
+
```mcfunction
|
|
646
|
+
execute as @e[type=minecraft:zombie] run function ns:main/foreach_0
|
|
647
|
+
```
|
|
648
|
+
|
|
649
|
+
4. 子函数内部把绑定变量 `z` 映射成 `@s`
|
|
650
|
+
|
|
651
|
+
所以 `foreach` 本质上是“选择器 + execute as + 子函数”。
|
|
652
|
+
|
|
653
|
+
### `impl` 方法怎么降级?
|
|
654
|
+
|
|
655
|
+
`impl` 不会保留成一个独立运行时结构。
|
|
656
|
+
|
|
657
|
+
Lowering 会把方法名改写成普通函数名:
|
|
658
|
+
|
|
659
|
+
```text
|
|
660
|
+
impl Counter {
|
|
661
|
+
fn inc(self, n: int) {}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
==>
|
|
665
|
+
|
|
666
|
+
Counter_inc(self, n)
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
这样后面的 CodeGen 不需要理解“面向对象”,只需要按普通函数生成就行。
|
|
670
|
+
|
|
671
|
+
## 5. 代码生成 (CodeGen)
|
|
672
|
+
|
|
673
|
+
### IR → mcfunction
|
|
674
|
+
|
|
675
|
+
CodeGen 的工作是把 IR 指令翻成真正的 Minecraft 命令。
|
|
676
|
+
|
|
677
|
+
例如:
|
|
678
|
+
|
|
679
|
+
```text
|
|
680
|
+
assign x, 5
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
会生成:
|
|
684
|
+
|
|
685
|
+
```mcfunction
|
|
686
|
+
scoreboard players set $x rs 5
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
再例如:
|
|
690
|
+
|
|
691
|
+
```text
|
|
692
|
+
cmp t0, a, >, b
|
|
693
|
+
```
|
|
694
|
+
|
|
695
|
+
会生成两步:
|
|
696
|
+
|
|
697
|
+
```mcfunction
|
|
698
|
+
scoreboard players set $t0 rs 0
|
|
699
|
+
execute if score $a rs > $b rs run scoreboard players set $t0 rs 1
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
因为 Minecraft 没有“比较后直接得到布尔值”的指令,只能先清零,再条件赋 1。
|
|
703
|
+
|
|
704
|
+
### `tick.json` / `load.json` 生成
|
|
705
|
+
|
|
706
|
+
CodeGen 不只是生成函数,还要生成 datapack 入口。
|
|
707
|
+
|
|
708
|
+
#### `__load.mcfunction`
|
|
709
|
+
|
|
710
|
+
它负责:
|
|
711
|
+
|
|
712
|
+
- 创建运行时 scoreboard objective `rs`
|
|
713
|
+
- 初始化全局变量
|
|
714
|
+
- 注册 trigger objective
|
|
715
|
+
- 初始化某些事件检测用 objective
|
|
716
|
+
- 调用所有 `@load` 函数
|
|
717
|
+
|
|
718
|
+
并在:
|
|
719
|
+
|
|
720
|
+
```text
|
|
721
|
+
data/minecraft/tags/function/load.json
|
|
722
|
+
```
|
|
723
|
+
|
|
724
|
+
里注册:
|
|
725
|
+
|
|
726
|
+
```json
|
|
727
|
+
{ "values": ["<namespace>:__load"] }
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
#### `__tick.mcfunction`
|
|
731
|
+
|
|
732
|
+
它负责:
|
|
733
|
+
|
|
734
|
+
- 每 tick 调用所有 `@tick` 函数
|
|
735
|
+
- 检查 `@on_trigger(...)`
|
|
736
|
+
- 检查事件标签并分发 `@on(...)`
|
|
737
|
+
|
|
738
|
+
并在:
|
|
739
|
+
|
|
740
|
+
```text
|
|
741
|
+
data/minecraft/tags/function/tick.json
|
|
742
|
+
```
|
|
743
|
+
|
|
744
|
+
里注册:
|
|
745
|
+
|
|
746
|
+
```json
|
|
747
|
+
{ "values": ["<namespace>:__tick"] }
|
|
748
|
+
```
|
|
749
|
+
|
|
750
|
+
### 目录结构
|
|
751
|
+
|
|
752
|
+
典型输出结构如下:
|
|
753
|
+
|
|
754
|
+
```text
|
|
755
|
+
pack.mcmeta
|
|
756
|
+
data/
|
|
757
|
+
minecraft/
|
|
758
|
+
tags/
|
|
759
|
+
function/
|
|
760
|
+
load.json
|
|
761
|
+
tick.json
|
|
762
|
+
mypack/
|
|
763
|
+
function/
|
|
764
|
+
__load.mcfunction
|
|
765
|
+
__tick.mcfunction
|
|
766
|
+
main.mcfunction
|
|
767
|
+
main/then_0.mcfunction
|
|
768
|
+
main/else_0.mcfunction
|
|
769
|
+
__trigger_x_dispatch.mcfunction
|
|
770
|
+
advancements/
|
|
771
|
+
on_advancement_x.json
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
为什么会有 `main/then_0.mcfunction` 这种文件?
|
|
775
|
+
|
|
776
|
+
因为控制流被拆成多个基本块后,每个块都可以落成一个单独的 `.mcfunction` 文件,前一个块用 `function` 跳过去。
|
|
777
|
+
|
|
778
|
+
## 6. 优化器
|
|
779
|
+
|
|
780
|
+
RedScript 里有两层优化:
|
|
781
|
+
|
|
782
|
+
- AST 层的死代码消除
|
|
783
|
+
- 命令层的优化,比如公共子表达式和 `setblock` 批处理
|
|
784
|
+
|
|
785
|
+
### 6.1 死代码消除 (DCE)
|
|
786
|
+
|
|
787
|
+
#### 标记-清除算法
|
|
788
|
+
|
|
789
|
+
DCE 的核心思路是:先找“肯定会用到”的东西,再把没用到的删掉。
|
|
790
|
+
|
|
791
|
+
步骤:
|
|
792
|
+
|
|
793
|
+
1. 找入口点
|
|
794
|
+
2. 从入口点出发,递归标记会调用到的函数
|
|
795
|
+
3. 记录哪些常量、局部声明真的被读取
|
|
796
|
+
4. 清除未使用内容
|
|
797
|
+
|
|
798
|
+
伪代码:
|
|
799
|
+
|
|
800
|
+
```text
|
|
801
|
+
findEntryPoints()
|
|
802
|
+
for each entry:
|
|
803
|
+
markReachable(entry)
|
|
804
|
+
|
|
805
|
+
markReachable(fn):
|
|
806
|
+
if fn already marked: return
|
|
807
|
+
mark fn
|
|
808
|
+
scan fn body
|
|
809
|
+
if fn calls other function:
|
|
810
|
+
markReachable(other)
|
|
811
|
+
```
|
|
812
|
+
|
|
813
|
+
#### 入口点追踪
|
|
814
|
+
|
|
815
|
+
项目里会把这些当成入口点:
|
|
816
|
+
|
|
817
|
+
- `main`
|
|
818
|
+
- `@tick`
|
|
819
|
+
- `@load`
|
|
820
|
+
- `@on(...)`
|
|
821
|
+
- `@on_trigger(...)`
|
|
822
|
+
- `@on_advancement(...)`
|
|
823
|
+
- `@on_craft(...)`
|
|
824
|
+
- `@on_death`
|
|
825
|
+
- `@on_login`
|
|
826
|
+
- `@on_join_team`
|
|
827
|
+
|
|
828
|
+
也就是说,就算某个事件函数没有被普通函数显式调用,它也不会被删掉。
|
|
829
|
+
|
|
830
|
+
ASCII 图:
|
|
831
|
+
|
|
832
|
+
```text
|
|
833
|
+
入口点
|
|
834
|
+
├─ main
|
|
835
|
+
├─ tick_fn
|
|
836
|
+
└─ on_die
|
|
837
|
+
|
|
838
|
+
从这些点出发做图遍历:
|
|
839
|
+
|
|
840
|
+
main ----> helper_a ----> helper_b
|
|
841
|
+
tick_fn -> helper_b
|
|
842
|
+
on_die --> reward_player
|
|
843
|
+
|
|
844
|
+
没有任何边连到的 unused_fn
|
|
845
|
+
=> 删除
|
|
846
|
+
```
|
|
847
|
+
|
|
848
|
+
### 6.2 setblock 批处理
|
|
849
|
+
|
|
850
|
+
这是一个非常实用的命令级优化。
|
|
851
|
+
|
|
852
|
+
#### 相邻方块检测
|
|
853
|
+
|
|
854
|
+
优化器会扫描一串命令:
|
|
855
|
+
|
|
856
|
+
```mcfunction
|
|
857
|
+
setblock 0 64 0 stone
|
|
858
|
+
setblock 1 64 0 stone
|
|
859
|
+
setblock 2 64 0 stone
|
|
860
|
+
```
|
|
861
|
+
|
|
862
|
+
如果发现它们:
|
|
863
|
+
|
|
864
|
+
- 方块类型相同
|
|
865
|
+
- `y` 相同
|
|
866
|
+
- 在 `x` 或 `z` 轴连续相邻
|
|
867
|
+
|
|
868
|
+
就认为它们可以合并。
|
|
869
|
+
|
|
870
|
+
#### fill 命令合并
|
|
871
|
+
|
|
872
|
+
上面的三条会被改写成:
|
|
873
|
+
|
|
874
|
+
```mcfunction
|
|
875
|
+
fill 0 64 0 2 64 0 stone
|
|
876
|
+
```
|
|
877
|
+
|
|
878
|
+
伪代码:
|
|
879
|
+
|
|
880
|
+
```text
|
|
881
|
+
scan commands from left to right
|
|
882
|
+
if current is setblock:
|
|
883
|
+
start a run
|
|
884
|
+
keep extending while next block is adjacent and same type
|
|
885
|
+
if run length >= 2:
|
|
886
|
+
replace run with one fill command
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
ASCII 图:
|
|
890
|
+
|
|
891
|
+
```text
|
|
892
|
+
setblock(0,64,0,stone)
|
|
893
|
+
setblock(1,64,0,stone)
|
|
894
|
+
setblock(2,64,0,stone)
|
|
895
|
+
|
|
|
896
|
+
v
|
|
897
|
+
fill(0,64,0 -> 2,64,0, stone)
|
|
898
|
+
```
|
|
899
|
+
|
|
900
|
+
## 7. 特色功能实现
|
|
901
|
+
|
|
902
|
+
### 7.1 impl 块
|
|
903
|
+
|
|
904
|
+
#### 方法解析
|
|
905
|
+
|
|
906
|
+
类型检查阶段会先把 `impl` 里的方法登记到:
|
|
907
|
+
|
|
908
|
+
```text
|
|
909
|
+
implMethods[typeName][methodName] = FnDecl
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
这样当编译器看到:
|
|
913
|
+
|
|
914
|
+
```rs
|
|
915
|
+
Counter::new()
|
|
916
|
+
```
|
|
917
|
+
|
|
918
|
+
或
|
|
919
|
+
|
|
920
|
+
```rs
|
|
921
|
+
state.inc(1)
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
就能知道应该解析到哪个方法。
|
|
925
|
+
|
|
926
|
+
#### self 参数处理
|
|
927
|
+
|
|
928
|
+
这里分三步:
|
|
929
|
+
|
|
930
|
+
1. Parser:如果 `impl` 方法第一个参数是 `self`,会自动补成结构体类型
|
|
931
|
+
2. TypeChecker:检查 `self` 是否真的是第一个参数,类型是否匹配当前 `impl`
|
|
932
|
+
3. Lowering:把方法改写成普通函数,比如 `Counter_inc`
|
|
933
|
+
|
|
934
|
+
简化示例:
|
|
935
|
+
|
|
936
|
+
```rs
|
|
937
|
+
impl Counter {
|
|
938
|
+
fn inc(self, by: int) {}
|
|
939
|
+
}
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
等价于:
|
|
943
|
+
|
|
944
|
+
```text
|
|
945
|
+
FnDecl(name="inc", params=[self: Counter, by: int])
|
|
946
|
+
Lowered name = Counter_inc
|
|
947
|
+
```
|
|
948
|
+
|
|
949
|
+
### 7.2 事件系统 `@on(Event)`
|
|
950
|
+
|
|
951
|
+
#### 标签检测
|
|
952
|
+
|
|
953
|
+
RedScript 目前把事件类型定义在 `src/events/types.ts` 里。
|
|
954
|
+
|
|
955
|
+
例如:
|
|
956
|
+
|
|
957
|
+
- `PlayerDeath`
|
|
958
|
+
- `PlayerJoin`
|
|
959
|
+
- `BlockBreak`
|
|
960
|
+
- `EntityKill`
|
|
961
|
+
- `ItemUse`
|
|
962
|
+
|
|
963
|
+
每个事件都会绑定一个 tag,例如:
|
|
964
|
+
|
|
965
|
+
```text
|
|
966
|
+
PlayerDeath -> rs.just_died
|
|
967
|
+
PlayerJoin -> rs.just_joined
|
|
968
|
+
```
|
|
969
|
+
|
|
970
|
+
TypeChecker 会检查:
|
|
971
|
+
|
|
972
|
+
- 事件名是否合法
|
|
973
|
+
- 处理函数参数是否符合事件签名
|
|
974
|
+
|
|
975
|
+
Lowering 会把事件信息挂到 IR 函数元数据里:
|
|
976
|
+
|
|
977
|
+
```text
|
|
978
|
+
eventHandler = {
|
|
979
|
+
eventType: "PlayerDeath",
|
|
980
|
+
tag: "rs.just_died"
|
|
981
|
+
}
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
#### tick dispatcher
|
|
985
|
+
|
|
986
|
+
CodeGen 在生成 `__tick.mcfunction` 时,会把这些事件处理器统一串起来:
|
|
987
|
+
|
|
988
|
+
```mcfunction
|
|
989
|
+
execute as @a[tag=rs.just_died] run function ns:on_die
|
|
990
|
+
tag @a[tag=rs.just_died] remove rs.just_died
|
|
991
|
+
```
|
|
992
|
+
|
|
993
|
+
也就是:
|
|
994
|
+
|
|
995
|
+
```text
|
|
996
|
+
游戏里某处给玩家打 tag
|
|
997
|
+
|
|
|
998
|
+
v
|
|
999
|
+
__tick.mcfunction 扫描 tag
|
|
1000
|
+
|
|
|
1001
|
+
v
|
|
1002
|
+
执行对应 RedScript 事件函数
|
|
1003
|
+
|
|
|
1004
|
+
v
|
|
1005
|
+
移除 tag,避免重复触发
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
这是一个很简单但很稳的事件分发模型。
|
|
1009
|
+
|
|
1010
|
+
补充一点:
|
|
1011
|
+
|
|
1012
|
+
- `@on_trigger("x")` 不是 tag 模式,而是 scoreboard trigger objective
|
|
1013
|
+
- `@on_advancement(...)`、`@on_craft(...)`、`@on_death` 会额外生成 advancement JSON
|
|
1014
|
+
|
|
1015
|
+
### 7.3 f-string
|
|
1016
|
+
|
|
1017
|
+
#### 编译为 tellraw JSON
|
|
1018
|
+
|
|
1019
|
+
RedScript 的 f-string 不是在编译期简单拼接字符串,而是会转成 `tellraw` 用的 JSON 数组。
|
|
1020
|
+
|
|
1021
|
+
例如:
|
|
1022
|
+
|
|
1023
|
+
```rs
|
|
1024
|
+
tellraw(@a, f"HP: {hp}");
|
|
1025
|
+
```
|
|
1026
|
+
|
|
1027
|
+
会被拆成:
|
|
1028
|
+
|
|
1029
|
+
```text
|
|
1030
|
+
[
|
|
1031
|
+
"",
|
|
1032
|
+
{"text":"HP: "},
|
|
1033
|
+
{"score":{"name":"$hp","objective":"rs"}}
|
|
1034
|
+
]
|
|
1035
|
+
```
|
|
1036
|
+
|
|
1037
|
+
Lowering 的处理流程是:
|
|
1038
|
+
|
|
1039
|
+
1. 识别 `say`、`tellraw`、`title` 这些“富文本内建函数”
|
|
1040
|
+
2. 如果消息参数是 `f_string` 或 `str_interp`
|
|
1041
|
+
3. 调用 `buildRichTextJson`
|
|
1042
|
+
4. 把文本片段和表达式片段分别转成 JSON component
|
|
1043
|
+
|
|
1044
|
+
ASCII 图:
|
|
1045
|
+
|
|
1046
|
+
```text
|
|
1047
|
+
f"HP: {hp}"
|
|
1048
|
+
|
|
|
1049
|
+
+--> text("HP: ")
|
|
1050
|
+
+--> expr(hp)
|
|
1051
|
+
|
|
|
1052
|
+
v
|
|
1053
|
+
scoreboard component
|
|
1054
|
+
|
|
|
1055
|
+
v
|
|
1056
|
+
tellraw JSON array
|
|
1057
|
+
```
|
|
1058
|
+
|
|
1059
|
+
简化伪代码:
|
|
1060
|
+
|
|
1061
|
+
```text
|
|
1062
|
+
buildRichTextJson(fstring):
|
|
1063
|
+
parts = [""]
|
|
1064
|
+
for part in fstring.parts:
|
|
1065
|
+
if part is text:
|
|
1066
|
+
parts.push({text: value})
|
|
1067
|
+
else:
|
|
1068
|
+
parts.push(convertExprToJson(part.expr))
|
|
1069
|
+
return JSON.stringify(parts)
|
|
1070
|
+
```
|
|
1071
|
+
|
|
1072
|
+
## 小结
|
|
1073
|
+
|
|
1074
|
+
如果把整个编译器浓缩成一句话,可以这样理解:
|
|
1075
|
+
|
|
1076
|
+
```text
|
|
1077
|
+
RedScript 编译器做的事,
|
|
1078
|
+
就是把“像普通语言一样写的游戏逻辑”,
|
|
1079
|
+
一步步翻译成“Minecraft 能执行的 scoreboard / execute / function 命令”。
|
|
1080
|
+
```
|
|
1081
|
+
|
|
1082
|
+
其中最关键的设计是三点:
|
|
1083
|
+
|
|
1084
|
+
- 前端用 `Token -> AST` 保存源码结构
|
|
1085
|
+
- 中间层用 IR 抹平高级语法
|
|
1086
|
+
- 后端把变量、控制流、事件系统映射到 Minecraft 原生命令
|
|
1087
|
+
|
|
1088
|
+
所以它虽然看起来像一个普通编译器,但本质上是在给 Minecraft 这台“很奇怪的虚拟机”做代码翻译。
|