Skip to content

编译管线

#[roplat::system] 宏在编译期完成从 DSL 到可执行 async Rust 代码的全部转换。本文详解这一过程。

总览

graph TB
    subgraph "Phase 1: 解析"
        A["用户代码<br/>#[roplat::system] async fn main()"] --> B["DSL Parser"]
        B --> C["Scope 树"]
    end

    subgraph "Phase 2: 图构建"
        C --> D["FlowGraph"]
        D --> E["ReducibleGraph"]
    end

    subgraph "Phase 3: 分析"
        E --> F["拓扑排序"]
        F --> G["死锁检测 (DFS)"]
        G -->|"有环路"| ERR["编译错误"]
    end

    subgraph "Phase 4: 规约"
        G -->|"无环路"| H["图规约 R0-R4"]
        H --> I["Schedule"]
    end

    subgraph "Phase 5: 代码生成"
        I --> J["TokenStream"]
        J --> K["async Rust 代码"]
    end

Phase 1: DSL 解析

#[roplat::system] 接收用户写的函数体,解析其中的特殊语法:

#[roplat::system]
async fn main() {
    let mut a = NodeA::new();
    let mut b = NodeB::new();
    let mut c = NodeC::new();

    timer >> {
        a >> b >> c;
    };
}

解析器识别以下结构:

语法 解析为
let mut x = Expr; 节点声明(收集到顶层 scope)
rhythm >> { ... } 节律域(ScopedGroup)
a >> b 数据流边(FlowEdge)
match / if 条件分支节点

产出:一棵 Scope 树,每个节律域是一个 scope,scope 内记录节点列表和边。

Phase 2: 图构建

Scope 树被转换为 FlowGraph

  • 每个节点变成图中的顶点
  • 每条 >> 变成有向边
  • 跨 scope 的引用标记为跨域边
FlowGraph {
    nodes: [a, b, c],
    edges: [(a→b), (b→c)],
    groups: [Group { rhythm: timer, nodes: [a, b, c] }]
}

然后包装为 ReducibleGraph,这是图规约算法的工作对象。

Phase 3: 拓扑分析

依赖排序

对每个 scope 内的节点进行拓扑排序,确保执行顺序正确。

死锁检测

对跨域通道依赖进行 DFS 分析:

Group 0 (timer_1khz): [sensor, ctrl]
Group 1 (timer_30hz): [camera, detect]

跨域边: ctrl → detect (Group 0 → Group 1)
         detect → ctrl (Group 1 → Group 0)  ← 构成环路!

error: 检测到跨组死锁环路: [group_0] → [group_1] → [group_0]

环路检测在编译期完成——不存在运行时死锁的可能。

Phase 4: 图规约

核心算法反复应用 R0-R4 规则,将图化简为单个调度节点:

R0: 单节点规则

[A] → A.process(input).await

R1: 串行融合

A → B → C
SeqNode([A, B, C])

生成:
  let o1 = a.process(input).await;
  let o2 = b.process(o1).await;
  c.process(o2).await;

R2: 并行融合

    ┌→ B
A ──┤
    └→ C

ParNode(A, [B, C])

生成:
  let out = a.process(input).await;
  let (rb, rc) = tokio::join!(
      b.process(out.clone()),
      c.process(out),
  );

R3: 拓扑排序

对无法直接融合的复杂图,按拓扑序排列,然后递归应用 R1/R2。

R4: 通道切割

跨域边被替换为 channel Tx/Rx:

Group 0: [..., A]  →  Group 1: [B, ...]
         跨域边 A→B


Group 0: [..., A, Tx]    // A 的输出写入 channel
Group 1: [Rx, B, ...]    // B 从 channel 读取

通道类型根据拓扑自动选择:

  • 1:N 扇出 → triple buffer (SPMC)
  • 1:1 队列 → ring buffer (SPSC)

Phase 5: 代码生成

规约完成后,Schedule 被展开为 TokenStream——完全确定的、无动态调度的 async Rust 代码。

最终生成的代码等价于手写的 async fn,没有 dyn、没有 Box、没有运行时调度器。

优化效果

指标 传统方案(如 ROS 2) Roplat
每 tick 调度开销 ~10μs(executor 查找 + 虚调用) ~0ns(内联 await 链)
堆分配 每消息 1+ 次 0 次
类型检查 运行时反序列化 编译期
死锁发现 部署后才暴露 cargo build

返回 架构参考总览