lit 是 llvm 生态中专为编译器工具设计的声明式测试驱动器,通过解析源文件中的 // run: 和 // check: 注释执行命令并比对输出,不运行 c++ 单元测试;其核心用于验证 ir 变换结果,需配合 -load-pass-plugin 加载 pass 动态库,并严格匹配指令名与注册名。

lit 是什么,为什么不能直接用 make test
lit 不是测试框架的“替代品”,而是 LLVM 生态里专为编译器类工具设计的声明式测试驱动器。make test 或 ninja check-all 底层调的就是 lit,但它本身不运行 C++ 单元测试,也不启动进程断言;它读的是源文件里以 // RUN:、// CHECK: 开头的注释指令,然后执行命令、捕获输出、按行比对。你写一个自定义 Pass,最自然的验证方式就是:给一段 IR 输入,跑一遍 Pass,检查输出 IR 是否符合预期——这正是 lit + FileCheck 的强项。
// RUN: 行怎么写才不会被 opt 找不到 Pass
关键在于路径和注册。如果你的 Pass 名叫 MyOptPass,编译后生成动态库 libMyOptPass.so(Linux)或 libMyOptPass.dylib(macOS),那 // RUN: 必须显式加载它,并确保 opt 能定位到符号:
// RUN: opt -load-pass-plugin=./libMyOptPass.so -passes="my-opt-pass" -S %s | FileCheck %s-
-load-pass-plugin路径必须是相对当前测试文件的路径(不是构建目录),建议把测试文件放在test/Transforms/MyOptPass/下,和libMyOptPass.so放同级或子目录 - Pass 名称
my-opt-pass必须和你在registerPass里传入的PassRegistry::registerPass<myoptpass>("my-opt-pass", ...)</myoptpass>一致,大小写敏感 - 如果用
-load(旧式接口),需确认 Pass 已通过LLVM_PASS_PLUGIN_DEFINE宏导出,但推荐统一用-load-pass-plugin(LLVM 14+ 默认路径机制)
// CHECK: 匹配失败的常见原因
FileCheck 默认是“顺序严格匹配”,不是 grep。比如你期望某行出现 %call = call i32 @foo(),但实际输出里它前面多了一行 debug info 或空行,// CHECK: %call = call i32 @foo() 就会失败。常用对策:
- 用
// CHECK-NEXT:强制下一行,// CHECK-SAME:要求在同一行继续匹配 - 用
// CHECK-LABEL:划定函数边界,避免跨函数误匹配 - 变量捕获用
{{.*}}或{{%[a-z0-9]+}},例如// CHECK: store i32 {{.*}}, i32* %{{.*}} - 不要在
// CHECK:行末加空格——FileCheck会严格比对空白符 - 调试时加
-v参数:lit -v test/Transforms/MyOptPass/basic.ll,它会打印实际输出和每条 CHECK 的匹配位置
测试文件结构要和 LLVM 主干保持一致
LLVM 的 test/ 目录有固定约定,不照做会导致 lit 扫不到你的测试,或 CI 报路径错误:
- 测试文件名必须以
.ll结尾,内容是合法的 LLVM IR(可用clang -S -emit-llvm生成) - 目录结构建议模仿已有 Pass:比如你的 Pass 属于优化类,就放
llvm/test/Transforms/MyOptPass/;如果是分析类,走Analysis/ - 每个测试文件顶部加
REQUIRES: myoptpass(可选),并在llvm/test/lit.cfg.py里注册该 feature,否则 CI 可能跳过 - 不要在测试文件里写 C++ 代码或调用系统命令——
lit只认// RUN:启动的工具链流程
最易被忽略的一点:FileCheck 的匹配是基于文本行号的,而 opt 输出的 IR 可能因版本差异插入额外 metadata 行(如 !dbg)。一旦你本地测试通过,CI 失败,八成是这个原因——得用 // CHECK-NOT: 过滤掉不确定行,或改用 // CHECK-LABEL: 锚定关键段落。











