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
@@ -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 这台“很奇怪的虚拟机”做代码翻译。