10.6 代码生成原理

> **本节目标**:理解 RML 编译器如何把 `.rml` 模板转换为 GPUI 渲染代码,掌握 AST、语义验证、代码生成的完整链路。 > > **语法说明**:RML 指令为 `if` / `each` / `show`(无 `r:` 前缀),双向绑定为 `value={field}`,事件为 `on-click={fn}`。下文若出现 `r:if` 等仅为 AST 说明中的历史标记,非实际写法。

10.6.1 编译流水线

┌──────────────────────────────────────────────────────────┐
│                  RML 编译流水线                             │
│                                                          │
│  .rml 源码                                                │
│      │                                                   │
│      ▼  词法分析 + 语法分析                                  │
│  AST(抽象语法树)                                          │
│      │                                                   │
│      ▼  语义分析                                           │
│  类型化 AST(带绑定路径、命令引用、组件引用)                    │
│      │                                                   │
│      ▼  优化(常量折叠、死代码消除)                          │
│  优化后的 AST                                              │
│      │                                                   │
│      ▼  代码生成                                           │
│  Rust 源码(实现 Render trait)                            │
│      │                                                   │
│      ▼  格式化                                            │
│  格式化的 Rust 源码 → OUT_DIR                              │
└──────────────────────────────────────────────────────────┘

10.6.2 词法与语法分析

RML 使用基于 HTML 子集的词法分析器,产出 Token 流:

<div class="card" if={visible}>
  <p>{title}</p>
</div>

Token 流:

TagStart("div")
Attr("class", "card")
Attr("if", "visible")
TagEnd
Text("\n  ")
TagStart("p")
TagEnd
Interpolation("title")
TagClose("p")
Text("\n")
TagClose("div")
EOF

语法分析器把 Token 流构建为 AST:

enum Node {
    Element(Element),
    Text(SharedString),
    Interpolation(Expr),
    If { condition: Expr, body: Vec<Node> },
    Each { item: Ident, iterable: Expr, key: Option<Expr>, body: Vec<Node> },
}

struct Element {
    tag: TagName,
    attributes: Vec<Attribute>,
    children: Vec<Node>,
}

10.6.3 语义分析

语义分析把 AST 转为“类型化 AST”,解析所有引用:

绑定路径解析

`{user.name}` 中的 `user.name` 被解析为:

BindingPath {
    root: ViewModelField("user"),
    segments: vec![Field("name")],
    ty: Type::SharedString,
}

解析时校验:

- `user` 字段在 ViewModel 中存在且 `pub` - `User` 类型有 `name` 字段 - 类型与插值上下文兼容(`SharedString` 可渲染)

命令引用解析

`on-click={login}` 中的 `login` 被解析为:

CommandRef {
    method: "login",
    signature: Signature { event: ClickEvent, ... },
    vm: Type::LoginViewModel,
}

校验:

- `login` 方法存在且有 `#[command]` 宏 - 签名与 `ClickEvent` 兼容

组件引用解析

`<Button label="...">` 中的 `Button` 被解析为:

ComponentRef {
    ty: Type::Button,
    props: Vec<(PropName, PropValue)>,
}

校验:

- `Button` 组件存在 - 传入的 props 与组件定义匹配 - 必填 props 都已提供

10.6.4 优化

常量折叠

`if={true}` 被消除,body 直接内联:

// 折叠前
if true { <p>{title}</p> }
// 折叠后
<p>{title}</p>

死代码消除

`if={false}` 的 body 被完全移除,不生成代码。

静态子树提取

不含绑定的子树被标记为静态,生成时只构建一次:

// 静态子树缓存
static HEADER: Lazy<Element> = Lazy::new(|| {
    div().class("header").child(Label::new("Welcome"))
});

10.6.5 代码生成

代码生成器遍历优化后的 AST,产出 Rust 代码。

元素生成

<div class="card"><p>{title}</p></div>

生成:

div()
    .class("card")
    .child(
        p().child(self.title.clone()) // 绑定展开为字段访问
    )

if 生成

<p if={visible}>{title}</p>

生成:

if self.visible {
    Some(p().child(self.title.clone()).into_any_element())
} else {
    None
}

each 生成

<li each={item in items} key={item.id}>{item.title}</li>

生成:

self.items.iter().map(|item| {
    li().key(item.id).child(item.title.clone())
}).collect::<Vec<_>>()

绑定生成

`value={email}` 生成双向绑定:

input()
    .bind_value(
        cx.entity(),
        |vm| &vm.email,           // 读路径
        |vm, v| vm.email = v,     // 写路径
    )

命令生成

`on-click={login}` 生成事件监听:

button()
    .on_click(cx.listener(|this, ev: &ClickEvent, cx| {
        this.login(ev, cx)
    }))

10.6.6 Render 实现的组装

所有元素生成后,组装为 `Render` 实现:

impl Render for LoginView {
    fn render(&mut self, _window: &mut Window, cx: &mut ViewContext<Self>) -> impl IntoElement {
        let vm = self.viewmodel.read(cx);
        div()
            .class("login-form")
            .child(
                input()
                    .attr("type", "email")
                    .bind_value(cx.entity(), |vm| &vm.email, |vm, v| vm.email = v)
            )
            .child(
                input()
                    .attr("type", "password")
                    .bind_value(cx.entity(), |vm| &vm.password, |vm, v| vm.password = v)
            )
            .when(vm.error.is_some(), |d| {
                d.child(p().class("error").child(vm.error.clone().unwrap()))
            })
            .child(
                button()
                    .attr("disabled", vm.is_loading)
                    .on_click(cx.listener(|this, ev: &ClickEvent, cx| this.login(ev, cx)))
                    .child(if vm.is_loading { "登录中…" } else { "登录" })
            )
    }
}

10.6.7 绑定引擎的运行时

生成的代码在运行时与绑定引擎协作:

1. **首次渲染**:建立绑定订阅,ViewModel 字段变化时触发重渲染 2. **重渲染**:`cx.notify()` 后,绑定引擎标记脏绑定,下次 `render` 重算 3. **双向绑定**:UI 事件 → 写入 ViewModel 字段 → 触发 notify → 重渲染

// 生成的订阅注册(在 View 构造时)
cx.observe(&self.viewmodel, |this, cx| {
    cx.notify(); // ViewModel 变化时通知 View 重渲染
}).detach();

10.6.8 错误报告

编译器在语义分析阶段产出友好的错误:

error: 绑定路径不存在
  --> src/views/login/login.rml:5:12
   |
5 | <p>{user.emial}</p>
   |            ^^^^^
   |
   = help: ViewModel `LoginViewModel` 没有 `emial` 字段
   = note: 相似字段:`email`

错误包含:

- 文件名、行号、列号 - 高亮的源码片段 - 修复建议(如拼写相似的字段名)

10.6.9 调试编译器

打印 AST

RML_DUMP_AST=1 cargo build

输出每个 `.rml` 文件的 AST 到 `OUT_DIR/ast/`。

打印生成代码

cargo rml-expand views/login/login.rml

单步编译器

RML 编译器自身是 Rust 项目,可在 `rml-compiler` crate 中打断点调试。

10.6.10 编译器的扩展点

自定义指令

通过 `rml-compiler` 的插件 API 注册自定义指令:

rml_compiler::register_directive("r:auth", AuthDirective);

`AuthDirective` 实现 `Directive` trait,参与语义分析与代码生成。

自定义组件解析

默认组件解析基于 `#[component]` 宏注册的类型。可通过插件覆盖,实现动态组件加载等高级场景。

10.6.11 性能与缓存

编译器维护 `rml_cache.json`,记录每个文件的哈希与解析结果。未变化的文件:

- 跳过词法 / 语法分析 - 跳过语义分析 - 直接复用上次的生成代码

这使得大型项目的增量构建保持在秒级。

下一章 → [第 11 章 · 实战指南](../11-cookbook/INDEX.md)