Slang 编译器覆盖率恢复实战:覆盖率棘轮与文档驱动变体展开的三步方法论
2026/9/19 9:48:22 网站建设 项目流程

Slang 编译器覆盖率恢复实战:覆盖率棘轮与文档驱动变体展开的三步方法论

【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang

导读

本文复盘 Slang 仓库中一次针对自动生成测试套件docs/generated/tests/)的"隔夜覆盖率恢复行动":在文档范围重新生成(doc-scope regen)导致 slangc 编译器行覆盖率静默跌落 3,351 行之后,通过Step 1 覆盖率棘轮(ratchet)+ Step 2 变体矩阵广度展开 + Step 3 深度发射展开三步方法论,从低点恢复 1,257 行覆盖,并把剩余缺口精确定位为"文档受限(doc-limited)"而非"测试努力受限"。读完本文,你将掌握这套可复用的覆盖护栏工具的用法、文档驱动测试的硬性契约,以及如何把一个覆盖率缺口转化为可执行的文档充实积压清单。

一、背景:一次被"静默吞掉"的覆盖率回退

Slang 仓库的docs/generated/tests/是一套由文档驱动自动生成的测试套件(详见 regenerate.md),由_meta/regenerate.py驱动程序编排,按 manifest 中的 44 个 bundle 组织,分为conformance/(锚定 docs/language-reference/ 语言规范)、design/(锚定 docs/generated/design/ 设计文档)以及后补的coverage/(白盒表征测试,见 METHODOLOGY.md)三棵树。

2026 年 6 月的一次文档范围重新生成(PR #11490 的后续)暴露了一个严重问题:recreate-to-doc-scope 步骤直接删除了 985 个单目标发射.slang文件(566 个 spirv-asm、243 个 hlsl、68 个 metal、61 个 glsl、48 个 wgsl、12 个 cuda、1 个 torch)及 ir-reference 测试,导致覆盖 slangc 编译器的行覆盖率从53.10%(134,016 行)跌到 51.77%(130,665 行),净损失3,351 行——而整套 CI 没有任何一个环节失败。

正如 coverage-history.md 记录的:套件在 bootstrap、fork master 与 upstream master(PR #11421 之后)之间的覆盖率对比表明,上游 master 状态达到53.55% / 135,156 行,是"要超越的目标";而本次回退正是由于删除的发射深度集中在slang-emit-spirv.cpp(−671)、slang-ir-glsl-legalize.cpp(−640)、slang-ir-spirv-legalize.cpp(−144)、slang-ir-legalize-varying(−152)等发射与合法化后端文件上。

根因:这一整轮事故之所以能"不声不响"地合入,是因为缺失一件东西——对覆盖率的强制性护栏(enforcement)。这正是本次隔夜行动要补的缺口。

二、三步方法论总览与覆盖率轨迹

行动目标明确为"do 1 then 2 then 3, commit each, coverage run after each, write an analysis on each step",三步全部完成并各自提交、各自测量、各自产出一份分析报告(即 analysis-step1.md、analysis-step2.md、analysis-step3.md)。

所有测量使用同一份带覆盖率插桩的libslang.sobuild/RelWithDebInfo/,clang 基于源码的覆盖率),仅切换测试套件(git checkout <commit> -- docs/generated/tests/),因此数字可直接比较。完整轨迹如下:

阶段行覆盖率已覆盖行数
baseline(重生成前)53.10%134,016
重生成后(回退低点)51.77%130,665
+ 后端 fan-out(更早)52.00%131,242
Step 2 —— 变体广度52.06%131,410
Step 3 —— 深度发射52.27%131,922

从低点累计恢复+1,257 行(约占丢失 3,351 行的 37%);相对 baseline 仍差−2,094 行 / −0.83pp,且该剩余缺口被证明是文档受限(详见第五节)。

三、Step 1:覆盖率棘轮——补上缺失的"刹车"

Step 1 的交付物是 tools/coverage/coverage-floor.py 与 floors.json。这一步刻意不添加任何测试——它是后续 Step 2、3 的安全网(guardrail),而不是覆盖率本身。

3.1 工具契约:record/check两个子命令

# 从某次 llvm-cov 报告的 TOTAL 行记录基准 python3 tools/coverage/coverage-floor.py record <label-slangc-report.txt> [--reason TEXT] # 检查某次报告是否低于已记录基准 python3 tools/coverage/coverage-floor.py check <label-slangc-report.txt>

实现细节(coverage-floor.py):parse_total读取 llvm-cov 文本报告的TOTAL行,按列位置解析——Regions / MissedReg / Cov% / Func / MissedF / Exec% / Lines / MissedLines / Cov% / Branch / MissedBr / Cov%,即列 1–2 为 regions,列 7–8 为 lines,列 10–11 为 branches,covered = total − missed(这与 llvm-cov 报告第二行数字是 missed 而非 covered 的陷阱一致)。record{lines_covered, lines_total, regions_covered, branches_covered}连同reason写入floors.jsoncheck解析当前报告后逐项比对,任一指标低于基准即输出FAILexit 1(达标 exit 0,出错如找不到 TOTAL 行或未记录基准 exit 2)。

3.2 双向验证:护栏确实生效

分析文档记录了两种方向的验证结果:

  • 用 Step 1 自身的报告(= 基准)执行checkexit 0,输出 "coverage at or above floor";
  • 用 fan-out 之前的低点报告(130,665 行)执行checkexit 1,逐项报告lines_covered −577, regions −227, branches −249

也就是说,如果当初重生成时就有这道闸门,−3,351 行的跌落会在 CI 中被拦下,而不是默默合入。

3.3 设计取舍与注意事项

  • 套件级而非逐 bundle 级:runner 已产出合并的套件总量,逐 bundle 需要在各自 profile 中单独测量(68 次独立运行)。套件级足以捕获净回退且成本低;逐 bundle 被留作未来可能的细化。
  • 基准可带理由重记录record --reason ...):合法的编译器重构同样会移动覆盖率,棘轮不能成为"紧身衣"。纪律是"只有写下书面理由才能下调基准",而不是"永不下降"。
  • 基准刻意低于 baseline:Step 1 记录的是 131,242 行(= 当下状态),先锁住"不再继续下滑",再由 Step 2、3 逐步上调基准。

顺带一提:仓库中当前提交的 floors.json 已进一步演进到lines_covered: 141728(55.93% 行覆盖),reason 为 "master + breadth + depth + white-box coverage suite"——证明棘轮机制在隔夜行动之后仍被持续使用,并随白盒表征套件(coverage/树)的加入继续抬高。

四、Step 2:变体矩阵规则与广度展开

4.1 新方法论规则:文档列举什么,就测什么

Step 2 在 _claims.md §2 "Construct & legalization variants (depth)" 中固化了一条新规则:当文档描述某个转换或发射、且其处理按输入的类型/形状/形式分叉时(类型映射/布局表、opcode 或 decoration 列表、逐元素类型的数组步长、逐操作符的原子操作、逐地址空间的存储类别),应当为文档枚举的每一行/每一个分支实例化一个测试,而不是为整张表写一个测试。

这条规则配套 _expand.md "The hard rule":展开代理不接收任何源行信息——看不到未覆盖行、文件 diff、百分比;只能拿到 bundle 名、README、现有.slang文件与source_doc。原因是:如果测试针对"我们观察到未覆盖的源行"来写,就会把实现固化为规范,侵蚀套件"每条测试都映射到一个文档化断言"的价值。这条限制关乎来源(provenance)而非广度——它禁止靠看未覆盖代码选测试,但不禁止在权威表面(language-reference 类型表、core-module 声明*.meta.slang)确认某个变体族后系统性地实例化其成员。

4.2 十个最薄 bundle 的广度展开

Step 2 用regenerate.py expansion-candidates --from <report.json>(按 bundle 的平均覆盖率打分、只输出 bundle key + 分数、不泄漏源行细节)排序出10 个最薄的设计 bundle,包括pipeline/05-ir-passestarget-pipelines/{hlsl,cuda,spirv,metal}cross-cutting/targetspipeline/04-ast-to-irname-resolution/visibilitysyntax-reference/keywords-and-builtinspipeline/overview,产出+41 个测试(+168 行覆盖),全部intent=expansion、CHECK 行钉在真实 slangc 输出上,verify FAILED:0、lint 0/0。示例包括:05-ir-passes的 3 个文档化 pass 行(array-return-by-ref、reinterpret、defer)、hlsl 的矩阵复合选择与只读 matrix 结构缓冲、cuda 的SV_*→threadIdx/blockIdx与 active-mask 合成、metal +12 个测试。

4.3 关键发现:文档天花板(doc ceiling)

Step 2 最重要的结论不在测试数量,而在那个数字本身:41 个高质量测试只换来 +168 行覆盖,而且严格按文档工作的代理只找到2 处文档缺口。这不是代理不努力,而是文档天花板——代理正确地展开了文档枚举的一切,然后停下,因为契约禁止发明文档不支持的断言。

由此得出结论:剩余约 2,600 行的缺口是 doc-limited(文档受限),不是 effort-limited(测试努力受限)slang-emit-spirv(−610)、slang-ir-glsl-legalize(−630)、spirv-legalize(−142)等深分支由构造变体(逐类型数组步长、完整原子操作矩阵、逐类型/逐形状的发射形式)驱动,而设计文档的简洁章节(如spirv.md的 Phase D、GLSL 发射路径)并未枚举这些变体——在文档描述它们之前,无法写出契约合法的测试。这正是 Step 3 存在的原因。

五、Step 3:深度发射展开与文档充实积压

5.1 深度优先胜过广度优先

Step 3 在target-pipelines/{spirv,hlsl,metal,cuda,wgsl}五个 bundle 上做穷举式深度优先展开,实例化每个文档可枚举的发射变体(逐元素类型/形状/操作符),并同时在共享 SPIR-V/GLSL 合法化路径上发射到 spirv-asmglsl。产出+79 个测试(+512 行覆盖,是 Step 2 的 3 倍),分布为 spirv +16(完整原子操作族、bitcast、逐类型/形状的 ArrayStride、首批 GLSL 臂测试)、hlsl +17、metal +10、cuda +16、wgsl +20。

5.2 九项文档充实积压清单(the actionable backlog)

Step 3 的代理把"文档命名了通用规则但未枚举编译器实际分叉的变体族"记录为## Doc gaps observed,形成精确的9 项 doc-enrichment backlog

#文档缺口编译器实际行为(文档未覆盖的部分)
1SPIR-V 原子操作族文档只写了InterlockedAdd→OpAtomicIAdd;代码实际处理 And/Or/Xor/Exchange/CompareExchange 及有符号/无符号 Min/Max 拆分(OpAtomicSMin/UMin…)
2SPIR-V StructuredBuffer 的 std430 聚合布局文档只记录了 cbuffer 的*_std140StructuredBuffer<struct>实际发射带逐成员Offset*_std430StorageBuffer 布局
3HLSL 字节寻址 Load 依赖元素类型.Load(原生 uint 字)、模板化Load<T>、逐分量向量分解三者行为不同
4HLSL 复合选择(composite-select)文档只提 matrix/struct;array 类型的条件表达式被同样改写但未列出
5Metal 矩阵 device/structured 缓冲_MatrixStorage_包装只记录了 cbuffer,但RWStructuredBuffer<float4x4>也会被lowerBufferElementTypeToStorageType包装
6Metal 原子操作数族比文档化的atomic_uint更宽:atomic_int/ulong/long/float,device 与 threadgroup 两种地址空间;half/double 在类型检查时被拒(E36107)
7CUDA Interlocked→atomic* 映射完整家族(atomicAnd/Or/Xor/Min/Max/Exch/CAS)+ 操作数类型 × 存储目标的正交组合,远超文档化的atomicAdd
8WGSL 数值宽度矩阵i64/u64 与 f16(带enable f16;)受支持且可观察;double触发内部错误 abort而非干净诊断
9WGSL 窄整型int16/uint16给出干净诊断 E56103;int8/uint8触发内部错误 abort

5.3 附带产出:两个编译器 bug

第 8、9 项不只是文档缺口,而是编译器 bug:WGSL 发射double("unexpected: double type emitted")与int8/uint8("unexpected: 8 bit integer type emitted")会命中内部错误 abort(error[E99997]: Slang compilation aborted due to an exception of N5Slang13InternalErrorE,exit 255),而不是像 16 位路径那样给出干净的"不支持类型"诊断。仓库的 findings 目录中已有对应的结构化记录可佐证:types-double-member-metal-wgsl-emit-abort.yaml(double结构体成员发射到 Metal/WGSL 时 abort;HLSL/GLSL/SPIR-V 均干净发射)与 values-intcast-wgsl-8bit-internal-error.yaml(同一 shader 在 spirv-asm/hlsl/glsl/metal 上干净发射,仅 WGSL 后端 abort,已归类为 wrong-diagnostic 而非 wrong-codegen)。

按 METHODOLOGY.md 的纪律——crash/abort/internal-error 是 finding,永远不是测试——这两个 bug 没有转成测试(abort 会导致失败),而是留在 bundle 的 Untested/Doc-gaps 中,等待通过_meta/findings/渠道提交。

5.4 为什么"剩余缺口"要留给人工闭环

剩余约 2,094 行是 doc-limited 的:设计文档命名了通用规则但没有枚举编译器分叉的变体族。要闭合它,必须先充实设计文档本身。但充实必须走 design-doc 重生成渠道(docs/generated/design/是生成式源头),不能在无人值守时手工编辑生成的 source-of-truth——那既是循环论证(用同一输出同时写文档和测试),又会破坏其 freshness/review provenance 契约。这一立场与 regenerate.md 的 hand-edit policy 完全一致:.slang与 README 禁止手改,正确动作是改进 prompt、改进源文档、或改进 manifest,然后mark-fresh

六、结论与推荐后续动作

方法论至此闭环,且补上了此前缺失的关键一环:

深度 = 重读文档以枚举可枚举的变体(Step 2 规则)+ 对残余 bundle 深度优先展开(Step 3)+ 覆盖率棘轮守卫(Step 1)。

文档扎根的深度在 52.27% 处已基本穷尽;闭合最后 −0.83pp 需要先充实设计文档本身(即 9 项积压清单),这被刻意留给**人工在环(human-in-the-loop)**通过 design-doc 重生成渠道处理。

分析文档给出的三条推荐后续动作:

  1. 执行 9 项积压的设计文档充实,再做一轮最终展开——有望闭合大部分剩余的 −2,094 行;
  2. 把 2 个 WGSL 内部错误 abort 作为编译器 bug 提交(通过_meta/findings/regenerate.py findings file <id>流程);
  3. coverage-floor.py check接入 CI,让棘轮真正被强制执行(regenerate.py无第三方 Python 依赖,接入只需一次python3调用)。

本次会话的提交(位于2026-06-doc-update-1分支)包括:design 重生成 + freshness、coverage runner、20 个后端 fan-out bundle、size-caps/INDEX、coverage-fanout 审计、Step 1棘轮、variant-matrix 规则、Step 210 个展开 bundle + floor、Step 35 个深度展开 bundle + floor。

七、可继续深入的仓库资源

  • 覆盖率工具与数据:tools/coverage/coverage-floor.py、floors.json、coverage-history.md
  • 三步分析报告:analysis-step1.md、analysis-step2.md、analysis-step3.md
  • 方法论契约:_claims.md(claim 枚举 + 变体展开规则)、_expand.md(hard rule + 展开循环)、CAMPAIGN.md(campaign 编排)、METHODOLOGY.md(白盒表征树)
  • 驱动与流程:regenerate.md(regenerate.py全部子命令、Phase A–F 状态、hand-edit policy)
  • 被深度展开的 bundle 示例:target-pipelines/wgsl/README.md(27 条 spelling claims、功能覆盖表、Untested claims 分类、Doc gaps observed 表)
  • 编译器 bug findings:types-double-member-metal-wgsl-emit-abort.yaml、values-intcast-wgsl-8bit-internal-error.yaml

【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询