PAYATHON 2026

如何衡量 C 程序翻译为 Rust 后的结构相似度?

数据阿明

建议先分别解析 C 和 Rust,将需要比较的结构转换成共同表示,再从控制流、函数拆分、调用关系、数据结构和错误处理五个维度评分。

只比较字符串、原始 AST 或编译后的控制流图都不够。你既关心执行路径,也关心源码的表达方式,需要分开保留这些信息。

先确定什么变化算结构变化

可以先建立一份比较规则表。下面是一种适合当前需求的起点,具体权重由你的评估目标决定。

比较维度需要保留的信息应记录的变化
控制流循环类型、分支、嵌套、continue、break、returnfor 改为 iterator chain,分支被合并
函数拆分函数边界、对应关系一对一、拆分、合并、内联
调用图调用者与被调用者之间的边调用链缩短、新增中间函数、边被删除
数据结构struct 边界、字段、顺序、嵌套关系字段丢失、扁平化、拆分、重新嵌套
错误处理检查条件、提前返回、传播形式显式检查改为 ?,返回码改为 Result

例如,变量改名通常可以忽略,但应保留 for 改为 .for_each() 这样的差异。如果把 for 和 .for_each() 都归一化为同一个“循环”节点,就无法回答你最关心的问题。

用两层表示比较控制流

第一层:保留源码表达方式的结构树

从两种语言的 AST 中提取共同节点,同时保留语言相关属性。例如:

Function
  Loop(kind=for)
    Branch
      Continue
    Call
  Return

对于 Rust 的 iterator chain,可以记录为:

Function
  IteratorChain
    Filter
    ForEach
      Call
  Return

这两棵树可以用同一套规则比较,即使遍历效果可能相似,也不会被视为完全相同。

可以归一化空白、注释、局部变量名,以及规则允许忽略的括号差异。循环种类、语句顺序、嵌套关系、提前退出和错误传播形式则应保留。

第二层:控制流图 CFG

CFG 用来辅助比较分支连接、回边和退出路径。它有助于判断路径组织是否相似,但通常无法单独判断源码是否保留了 for 或显式错误检查。

CFG 的抽取阶段也会影响结果。源码级表示与经过 desugaring、内联或优化后的表示,可能具有不同的图结构。比较时应记录抽取阶段并固定处理规则,以免把编译处理造成的变化误记为翻译造成的变化。

对于当前目标,可以将源码结构树作为主要依据,CFG 作为补充指标。

先匹配函数,再比较调用图

不要直接按函数名比较两个程序。翻译可能改名,也可能引入辅助函数。

优先使用翻译过程提供的源码位置映射或函数映射。如果没有这些信息,可以结合名称、参数数量、函数体结构和调用邻居生成候选对应关系,并为不确定的匹配保留置信度。

映射需要允许以下情况:

  • 一对一:一个 C 函数对应一个 Rust 函数。
  • 一对多:原函数被拆成多个函数。
  • 多对一:多个原函数被合并。
  • 无独立对应函数:可能被删除、内联,或尚未成功匹配,需要进一步区分。

函数拆分本身也是评估结果,强制一对一匹配会把它掩盖。可以分别报告一对一保留比例、拆分与合并数量、未匹配比例。

完成映射后,再把调用图节点替换为共同的函数标识,比较调用边。若把拆分出的多个函数收缩成一个逻辑节点,应同时保留收缩前的结果,说明原始结构发生了什么变化。

用可解释的指标评分

结构树:树编辑距离

可以用树编辑距离衡量节点插入、删除和替换的代价。建议按你的目标设置代价,例如将“变量改名”设为零代价,将“for 改为 iterator chain”设为非零代价。

一种归一化方式是:

[ S_{tree}=1-\frac{d(T_C,T_R)}{D(T_C)+I(T_R)} ]

其中,(d) 是最小编辑代价,(D) 是删除整棵 C 结构树的代价,(I) 是插入整棵 Rust 结构树的代价。要求代价非负,并允许通过整树删除、插入完成转换;分母为零的情况需要单独约定。

这个分数描述指定规则下的结构接近程度。编辑代价改变,分数的含义也会改变,因此规则必须随结果一起保存。

调用图:匹配后的边 precision、recall 和 F1

设原程序调用边集合为 (E_C),翻译后为 (E_R),两者已经使用共同的函数标识,则:

[ P=\frac{|E_C\cap E_R|}{|E_R|},\qquad R=\frac{|E_C\cap E_R|}{|E_C|} ]

[ F1=\frac{2|E_C\cap E_R|}{|E_C|+|E_R|} ]

下面的 Python 示例只演示调用边评分,不包含解析和函数匹配:

def call_edge_f1(c_edges, rust_edges):
    c_edges = set(c_edges)
    rust_edges = set(rust_edges)
    total = len(c_edges) + len(rust_edges)

    if total == 0:
        return None  # 两边均无调用边,此指标不提供区分信息

    return 2 * len(c_edges & rust_edges) / total


c_edges = {("A", "B"), ("B", "C")}
rust_edges = {("A", "B"), ("A", "C")}

print(call_edge_f1(c_edges, rust_edges))  # 0.5

该指标比较的是直接调用边,不衡量所有路径特征。如果特别关注 A → B → C 这样的调用链,还可以比较固定长度路径或各节点的调用邻居。

函数指针、动态分派或外部调用可能无法被静态分析完整解析。应报告未解析调用的数量和处理方式,避免把“没解析出来”直接算成“翻译后被删除”。

数据结构和错误处理需要单独分析

对于 struct,可以先建立类型与字段映射,再比较字段保留率、字段顺序、嵌套路径和结构拆分情况。字段改名不一定意味着字段丢失,字段数量相同也不意味着结构保留。

还需要区分源码中的组织方式与实际内存布局。若关心字段和嵌套是否保留,比较类型结构即可;若关心偏移、对齐和 ABI,则需要额外固定目标平台与布局约定,不能根据源码外观推断。

错误处理应记录具体模式。下面的代码仅用于说明比较方式,假设 op() 的两种接口表达相应的失败条件:

int f(void) {
    int ret = op();
    if (ret < 0) {
        return -1;
    }
    return 0;
}
fn f() -> Result<(), Error> {
    op()?;
    Ok(())
}

结构报告应记录“显式条件检查与提前返回被改为 ? 传播”,而不是简单给出“相同”或“不同”。这段示例不证明两种实现语义等价;返回值含义和错误映射仍需另行核对。

最终报告应能定位差异

建议先输出五个维度的结果,再按需要计算加权总分:

[ S=\sum_i w_iS_i,\qquad w_i\geq0,\quad\sum_iw_i=1 ]

权重没有通用标准。当前需求强调源码形态,可以提高源码结构树和函数边界的权重。没有可比较内容的维度应标为不适用,并明确剩余权重如何调整。

实际落地可以按以下顺序进行:

  1. 固定需要保留和允许忽略的结构特征。
  2. 解析两种语言,提取结构树、函数、调用边和类型结构。
  3. 建立函数与类型映射,报告匹配覆盖率。
  4. 分维度评分,输出新增、删除和改写的位置。
  5. 用少量人工标注样例校准规则,检查指标是否符合你的判断。

初期可以先实现函数对应关系、循环与分支结构、调用边三个维度。每个分数都应附上具体差异,例如“此处 for 被改为 iterator chain”或“此调用链新增一个辅助函数”,以帮助判断翻译保留了哪些结构。结构得分的高低本身不能证明语义正确,也不代表 Rust 代码质量的高低。

备注:内容仅供参考。