You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何编写可维护的编译器各阶段测试?

这问题太接地气了——我之前做编译器测试的时候也踩过不少「预期结果写死导致测试频繁失效」的坑,结合Rust和通用编译器测试经验,给你分享几个实用思路:

核心方向:从「精确匹配」转向「语义等价验证」

你现在的测试是直接对比输出和预期结果,这种方式最大的问题是绑定了实现细节——只要你调整了输出的格式、内部结构(比如AST加个字段、IR换个指令名),测试就会失效,但其实核心功能并没有问题。所以我们要把测试的关注点从「输出长什么样」转到「输出做了什么/表达了什么语义」。

1. 词法分析(lex)阶段:聚焦有效token的语义

词法分析的核心是把源码转换成有意义的token序列,那些无关的元数据(比如行号、列号、空格注释)不应该成为测试的阻碍:

  • 忽略非关键元数据:在Rust里,不要直接assert_eq!(tokens, expected_tokens)(因为expected_tokens里的span字段很难写死),而是逐个断言token的类型和字面量,比如:
    for (actual, expected) in tokens.iter().zip(expected_kinds_and_literals) {
        assert_eq!(actual.kind, expected.0);
        assert_eq!(actual.literal, expected.1);
        // 跳过span等无关字段
    }
    
  • 鲁棒性测试:用带不同空格、换行、注释的输入测试,确保输出的有效token序列一致。比如输入let x=42;和let x = 42 ; // comment,得到的token序列应该是一样的(除了span)。

2. 语法分析(parse)阶段:验证AST的语义结构

AST的精确序列化(比如Debug输出、JSON)很容易因为结构调整失效,我们要关注的是AST表达的语法结构:

  • 用结构化断言:Rust里可以用assert_matches crate来精准匹配AST的核心结构,比如:
    assert_matches!(
        ast_root,
        AstNode::Function {
            name: "add",
            params: [AstParam { name: "a" }, AstParam { name: "b" }],
            body: AstNode::BinaryOp {
                op: BinaryOp::Add,
                left: AstNode::Ident("a"),
                right: AstNode::Ident("b")
            }
        }
    );
    
    这样即使你给Function节点加了return_type字段,只要核心的函数名、参数、body不变,测试依然通过。
  • 错误场景抓类别而非精确消息:比如测试语法错误时,断言错误的类型是「未闭合的括号」「未知标识符」,而不是精确的错误字符串(除非错误消息是产品级需求)。

3. 中间表示(IR)阶段:测试行为而非形式

IR的迭代通常比较频繁(比如新增优化、调整指令结构),所以测试要关注IR的执行行为:

  • 模拟执行IR:写一个简单的IR解释器,执行生成的IR,检查输入输出的结果是否符合预期。比如IR是计算1+2,不管它用了临时变量%t1还是直接运算,只要解释器输出3,测试就通过。
  • 抽象无关细节:忽略临时变量名、寄存器编号这类实现细节,只验证指令的依赖关系和计算逻辑。比如对于赋值指令,只要确认值的来源和目标正确就行,不用管临时变量叫什么。

4. 代码生成(generation)阶段:验证执行结果

目标代码(汇编、机器码、字节码)最容易因为优化、目标架构细节变化失效,所以:

  • 跑起来测:把生成的代码编译成可执行文件,运行它,检查返回值、标准输出、副作用(比如文件写入)是否符合预期。在Rust里可以用std::process::Command来执行程序并捕获输出:
    let output = Command::new("./generated_program")
        .output()
        .expect("Failed to run program");
    assert_eq!(output.status.code(), Some(0));
    assert_eq!(String::from_utf8_lossy(&output.stdout), "3\n");
    
  • 分层测试:如果生成的是汇编,可以用汇编模拟器执行验证;如果是字节码,可以用虚拟机执行,不用纠结汇编指令的顺序或格式。
通用测试优化技巧

除了分阶段调整,还有几个通用方法能让测试更稳定:

  • 参数化测试:用rstest之类的库,把多个测试案例(边界值、典型案例、错误案例)批量传入测试函数,这样即使某个案例因为格式变化失效,其他案例还能覆盖核心功能。
  • 快照测试的正确打开方式:如果用insta这类快照库,不要对整个输出做快照,只对语义关键部分做快照(比如AST的核心结构序列化)。更新快照前一定要确认是语义变化还是格式变化,避免误更。
  • 契约式测试:定义每个阶段的输入输出契约(比如「输入合法源码,lex阶段必须输出无错误的token序列」),测试契约而非具体实现。比如测试lex阶段不会崩溃,输出的有效token数量和预期一致。
  • 回归测试:每次修改后跑全量测试,把之前导致失效的案例加入测试集,避免重复踩坑。

总的来说,核心就是让测试关注“做对了什么”,而不是“做成了什么样”,这样你的编译器即使迭代优化,测试也能保持稳定,不用频繁修改预期结果。

内容的提问来源于stack exchange,提问作者Mistodon

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 03:34:00