深度解读 Slang 后端发射器特性化测试:coverage/emit 测试束如何钉住跨目标代码生成行为
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
本篇技术指南以 docs/generated/tests/coverage/emit/README.md 为骨架,系统剖析 Slang 编译器中一组针对目标后端发射器(slang-emit-c-like.cpp、slang-emit-glsl.cpp、slang-emit-spirv.cpp)的白盒特性化(characterization)测试束:它的设计动机、两次构建策略、13 个测试逐个钉住的发射行为、测试可观察性方法论,以及死代码与疑似错误输出的归档方式。读完你将掌握:这类测试与规格测试的本质区别、如何让"只在一撮输入下才可观察"的翻译表/合法化重写被测试命中,以及如何用覆盖率为编译器后端做"扫尾"。
一、什么是特性化测试,为什么发射器需要它
常规单元测试断言"代码应当做什么"(规范测试);特性化测试则断言"代码当前实际做什么"。二者的区别正是这个测试束的定位——它在 README 的 Intent 一节中被明确表述为:
White-box characterization tests for the target back-end emitters ... They pin thecurrent observed emitted textof CLI-reachable, otherwise-untested emit arms, not a spec.
也就是说,这组测试不是为发射器立规矩,而是把"当前 slangc/slangi 实际吐出的目标代码文本"用 filecheck 模式钉死,防止后续改动悄悄改变发射形态而不被察觉。它针对的三个文件在发射管线中的位置见 docs/generated/design/pipeline/06-emit.md:emitEntryPointsSource在合法化 IR 之后,通过CLikeSourceEmitter::getSourceLanguage(target)把目标映射到GLSLSourceEmitter、HLSLSourceEmitter、CUDASourceEmitter、MetalSourceEmitter、WGSLSourceEmitter等子类;SPIR-V 则走直接发射路径emitSPIRVFromIR(slang-emit-spirv.cpp)。文本发射共同继承自 slang-emit-c-like.cpp 中的CLikeSourceEmitter,而 GLSL 在 slang-emit-glsl.cpp 中覆盖其行为。
测试束的两条关键纪律:
- 所有发射 token 逐字复制自
source_commit对应的本地slangc/slangi,保证钉住的是真实输出; - 凡是当前输出被判定为错误的目标,刻意不钉,改为归档 finding(见"不可达缺口"一节),避免把 bug 固化进测试;因此束内没有任何测试携带
characterization-unverified=true。
二、测试束的两次构建:从"目标发散"到"翻译表扫尾"
第一轮:按目标间发散挑选构造(6 个测试,2026-06-11)
第一轮的挑选原则是"降低时按目标分叉":让一个 shader 同时命中三个发射器文件里尚未被覆盖的分支。六个测试分别覆盖:
MakeMatrixFromScalar元素扇出(标量→矩阵构造);- 位域内建函数拆分(原生指令 vs 展开的移位/掩码运算);
- 矩阵×向量
mul操作数重排(列主序差异); - 基于区域的
switch发射与共享 case 标签; - 矩阵收窄 reshape(4×4→3×3);
- 结构体按值返回 /
FieldExtract。
第二轮:按翻译表与合法化重写扫尾(7 个测试,2026-08-05)
第一轮漏掉的分支大多属于"只有一撮输入才能使其可观察"的类型:翻译表(table)需要一行一个声明、合法化重写(legalization rewrite)需要一个操作符一个分支,塞进同一个 shader 才能全部触发。第二轮正是为此设计——在完整测试套件度量下,7 个新测试把slang-emit-glsl.cpp的行覆盖率从 78.69% 提升到 81.01%,slang-emit-spirv.cpp从 87.28% 提升到 88.45%,合计新增执行 188 条此前从未执行的行。
覆盖率数据的获取方法与局限(方法论警示)
README 特意用一整段说明了覆盖率数字的来历,这对任何做"覆盖率驱动补测"的人都值得借鉴:
- 最初交给扫尾流程的 profile(
build/cov-agentic-clean/slang-test.profdata)报告为 glsl 56.03% / c-like 80.48% / spirv 73.68%,但它是不完整的——通过slang-test -use-test-server采集时,测试服务器子进程的覆盖率大量丢失,因此严重低估了tests/已有的覆盖; - 扫尾候选测试被重新对照一份重建的参考 profile(算子 profile + 对
tests/全量做进程内运行),读数变为 glsl 78.69% / c-like 83.03% / spirv 87.28%,只有在这个参考上仍能执行到未执行行的候选才被保留; - 两个起草的测试(GLSL 图像原子操作、显式
unknown/r64ui图像格式臂)实测新增行数为零,直接删除而未提交。
这套"先怀疑 profile 不完整、再重建参考、只保留有增量贡献的候选"的流程,正是第二轮测试没有变成噪声测试的原因。
三、13 个测试逐项解析
以下按主题分组展开 README 功能覆盖表(Functional coverage)中的 13 个测试,每个都给出其钉住的具体行为与对应源文件。
矩阵构造与重塑的发射形态
- make-matrix-from-scalar-fanout.slang:
float3x3(scalar)的kIROp_MakeMatrixFromScalar臂位于CLikeSourceEmitter::emitInstExpr(slang-emit-c-like.cpp)。它把标量展开为每个矩阵元素一个操作数(3×3 共 9 个),于是 HLSL/GLSL/Metal/WGSL 都得到标量重复 9 次的扁平构造器(float3x3(...)、mat3x3(...)、matrix<float,int(3),int(3)> (...)、mat3x3<f32>(...))。CUDA 不经过这个臂——其 prelude 提供makeMatrix模板(见 prelude/slang-cuda-prelude.h 第 979 行的单标量重载),因此发射为单参数调用makeMatrix<float, 3, 3>。SPIR-V 没有构造器语法,同一扇出表现为嵌套OpCompositeConstruct:标量三次复制成行向量,行向量再三次复制成 3×3。 - matrix-reshape-row-truncation.slang:
(float3x3)float4x4矩阵收窄不发射单个MatrixReshapetoken,而是用源矩阵前三行、每行.xyz截断构造小矩阵:HLSL 为float3x3(big[0].xyz, big[1].xyz, big[2].xyz),GLSL/Metal/WGSL 同理;SPIR-V 上同一形态表现为三次OpVectorShuffle(从第 0/1/2 行各取分量 0 1 2)喂给一个OpCompositeConstruct。
矩阵×向量 mul 的操作数重排
matrix-vector-mul-operand-order.slang 钉住默认列主序布局下的mul(M, v):HLSL 保留内建mul(M, v);GLSL/Metal/WGSL 因*为列主序而反转成v * M(这个重排正是 GLSL 发射器的被测行为);SPIR-V 发射原生OpVectorTimesMatrix。测试特意用 uniform 矩阵与向量避免常量折叠,并把结果 sink 进outputBuffer防止 DCE。
switch 的区域化发射与共享 case 标签
switch-fallthrough-shared-case.slang 钉住emitSwitchCaseSelectorsImpl与区域化 switch 发射的三种目标形态(case 0/1 为空共享一个标签,case 2 无 break 落入 default):
- HLSL:case 2 把 default 体内联复制一份(
+100后紧跟+1并 break); - GLSL/Metal:两个空 case 输出为相邻标签共享一个体,case 2 保持自然 C 语言 fall-through,default 体只发射一次(用
CHECK-NOT钉住"未被复制"); - SPIR-V:单个
OpSwitch,0 和 1 映射到同一 case 标签,default 标签为尾操作数; - WGSL:语言无 fall-through,两个空 case 折叠成一个逗号分隔的 selector 列表
case i32(0), i32(1):,case 2 像 HLSL 一样把 default 体复制进自己,编译器对这次重构报告E41026。
位域内建函数的目标分化
bitfield-extract-insert.slang 钉住bitfieldExtract/bitfieldInsert的四路分化:SPIR-V 发射原生OpBitFieldUExtract/OpBitFieldInsert;GLSL 发射内建bitfieldExtract/bitfieldInsert(offset/bits 操作数带 int 转换);HLSL/Metal/WGSL 没有位域内建,由 c-like 发射器展开为显式移位/掩码算术(如 HLSL 的>>(4U))&与~(((uint(1)<<2U)-uint(1)),WGSL 的v_0>>(u32(4)))。操作数来自tid.x(运行时值)以避免内建被常量折叠,结果 sink 进outputBuffer。
bool 向量逻辑运算的 GLSL 合法化
glsl-bool-vector-logical-legalization.slang 钉住GLSLSourceEmitter::_tryEmitLogicalBinOp及其委托的_emitLegalizedBoolVectorBinOp:GLSL 的&&/||/^^只定义在标量bool上,bvec3上的分量级逻辑运算不能直接发射,发射器改写为uvec3上的按位运算再转回:bvec3(uvec3(a)&uvec3(b))。对比目标被一并钉住:HLSL/Metal 直接在布尔向量上保留(a && b)/(a || b)/(a ^ b);SPIR-V 用OpLogicalAnd/OpLogicalOr/OpLogicalNotEqual(布尔异或表示为不等比较);WGSL 无向量&&,把短路形式降低为select(...)。
GLSL 图像格式推断
glsl-image-format-inference-by-element-type.slang 钉住_emitGLSLImageFormatModifier的推断半区(存储图像不带[format(...)]时):组件数决定通道前缀r/rg/rgba,元素基类型决定大小与类别后缀32f/16f/32ui/32i/16i/8ui,并联动_emitGLSLTypePrefix得到图像类型名image2D/uimage2D/iimage2D/i16image2D/u8image2D(窄类型还会拉入GL_EXT_shader_16bit_storage/GL_EXT_shader_8bit_storage)。最值得钉的一行是3 分量:GLSL 没有三通道存储格式,RWTexture2D<float3>不推断rgb32f,而是静默加宽到rgba32f。SPIR-V 在同一组声明上运行自己的推断且答案不同:float 行(float/float2/float3)共享一个OpTypeImage ... Unknown(既不看组件数也不看 32 位 float 元素类型),窄/整数行则两目标都推断(Rgba16f/Rg32ui/R32i/Rgba16i/Rgba8ui)——README 特别注明,早期曾断言"SPIR-V 不跑这个推断",实测证明是错的,因此现在两个目标都钉。
插值修饰符的 GLSL 拼写表
glsl-interpolation-modifiers.slang 钉住_maybeEmitInterpolationModifierText把IRInterpolationMode映射为 GLSL 限定符的非对称拼写表:nointerpolation→flat、linear→smooth,而noperspective/centroid/sample恰好与 HLSL 同名。对比目标:SPIR-V 表达为OpDecorate(Flat/NoPerspective/Centroid/Sample),且因为透视校正中心插值是默认,linear字段不发射任何 decoration——"缺失"同样是映射的一部分;HLSL 五个修饰符原样往返。Metal 与 WGSL 被刻意排除,因为两者当前对部分修饰符映射错误(见下文 findings)。
StructuredBuffer 数据布局映射
glsl-structured-buffer-data-layouts.slang 钉住_emitGLSLStructuredBuffer块头的数据布局开关:StructuredBuffer<T, L>的第二个类型参数Std140DataLayout/Std430DataLayout/ScalarDataLayout分别映射layout(std140)/layout(std430)/layout(scalar),且只有 scalar 需要GL_EXT_scalar_block_layout(纯 GLSL 450 没有 scalar 布局)。同一 shader 还顺带钉了两个相邻决策:只读StructuredBuffer加readonly而可写的RWStructuredBuffer不加;globallycoherent变成 GLSL 的coherent。SPIR-V 没有块布局关键字,同一信息用ArrayStride/Offsetdecoration 表达,globallycoherent拼为OpDecorate ... Coherent。
GLSL 上的 -track-liveness 发射
glsl-track-liveness-spirv-intrinsics.slang 钉住-track-liveness在 GLSL 目标的呈现方式:GLSL 没有活区间标记语法,发射器为每个被标记类型前向声明一个带GL_EXT_spirv_intrinsics的spirv_instruction(id = ...)函数——256 对应OpLifetimeStart、257 对应OpLifetimeStop(这两个 id 在 slang-ir-glsl-liveness.cpp 中构造),参数用spirv_by_reference+spirv_literal限定符,使调用能降低为正确的 SPIR-V 操作数;声明按类型展开,因此三个不同局部类型产生三个livenessStart_N/livenessEnd_N重载,标记以普通调用出现在活区间边界(livenessStart_N(v, 0))。shader 故意声明结构体局部、int 循环计数器和 float 累加器三种类型,并用循环体内活区间严格位于函数内部的临时量使结束标记具有观察价值。
SPIR-V 图像格式翻译表
spirv-image-format-table.slang 钉住SPIRVEmitContext::getSpvImageFormat——一张纯数据的ImageFormat→SpvImageFormat翻译表。27 个其余测试套件从未声明的[format("...")]属性各占一行声明,逐个断言OpTypeImage的格式操作数,并顺带钉住rgb10_a2ui→Rgb10a2ui与rgb10_a2→Rgb10A2之间真实存在的大小写差异;超出 SPIR-V 基础集合的格式使模块声明StorageImageExtendedFormats能力。GLSL 半区被一并钉住:_emitGLSLImageFormatModifier把每个属性变成layout(...)限定符,27 个格式全部有 GLSL 拼写,与 SPIR-V 逐行对照。
unorm/snorm 存储图像的位宽推断
spirv-unorm-snorm-image-format-by-width.slang 钉住ensureTextureType的unorm/snorm分支:存储图像无[format(...)]时,发射器在元素类型上找UNormAttr/SNormAttr,按组件数选 8 位归一化格式。8 个声明(unorm/snorm × 1/2/3/4 分量)只产生 6 个OpTypeImage:3 分量没有三通道归一化格式,unorm float3与unorm float4共享Rgba8(snorm 对同理共享Rgba8Snorm),并有CHECK-NOT守卫不存在的Rgb8。GLSL 路径因已知与 SPIR-V 分歧(两者都折叠到layout(rgba32f))而不钉。
结构体按值返回的端到端验证
struct-return-value-roundtrip.slang 是束内唯一的值级(而非文本形态级)验证:makePair按值返回Pair,调用方逐字段读取求和。它驱动结构体返回/FieldExtract发射路径,并在slangi上驱动 VM 的结构体值处理;//TEST:INTERPRET在本地解释执行,输入 7 得到 7+14=21,//CHECK: sum=21无需 GPU 或 FileCheck 即可验证。
四、测试可观察性方法学
这些测试反复使用几项技巧让"本来会被优化掉"的发射形态存活到 emit 阶段,值得提炼成可复用模式:
- 反常量折叠:操作数一律来自运行时来源(
SV_DispatchThreadID、uniform),保证构造/运算不被前端的常量折叠提前消掉; - 反死代码消除:结果统一写入
RWStructuredBuffer/RWTexture2D(sink 到outputBuffer),所有被测声明在入口点内被读取/写入,使 DCE 无法删掉任何一个被测对象; - 多入口点:SPIR-V 路径往往会在只读取少数元素时把构造折叠回源矩阵(如矩阵收窄测试的
computeMain),因此为 SPIR-V 单独准备一个消费整个矩阵的入口点(computeMainMul),强制被测值物化; CHECK-DAG/CHECK-NOT/CHECK-SAME组合:把每个发射 token 与其所属声明配对断言,误映射会表现为匹配失败而非相邻声明的误命中;对"缺失"(如 SPIR-V 的linear无 decoration、GLSL 的 default 体未被复制)用CHECK-NOT显式钉住缺失本身。
指令层面,文本形态测试使用//TEST:SIMPLE(filecheck=XXX):-target <target> -entry <entry> -stage <stage>并携带各目标的 filecheck 前缀;值级验证使用//TEST:INTERPRET(filecheck=CHECK):。整套束在 README 中确认不需要 GPU、DXC、nvrtc 或 Apple 工具链,因此没有任何测试携带requires-tool。
五、未被钉住的声明:unsupported-on-target 的两种含义
README 的 "Untested claims" 节记录每个被测声明在哪些目标上没有对应测试,且特意指出unsupported-on-target这个 reason token 承载两种截然不同的情形,Why 单元格总会说明是哪一种:
| 被测声明 | 未测目标 | 具体原因 |
|---|---|---|
RWTexture2D无[format]时的 GLSL 格式推断(分量 1/2/3/4 × float/half/uint/int/int16/uint8) | hlsl、metal、wgsl | 无法表达:HLSL/Metal 根本不携带存储图像格式(RWTexture2D<float>/texture2d<float, access::read_write>),无 token 可断言;WGSL 编不过int16_t4/uint8_t4行(E41400 静态断言失败、E38203 禁止的向量元素类型) |
| 各 HLSL 插值修饰符的 GLSL 拼写 | metal、wgsl | 输出疑似错误而非无法表达:Metal 把linear→[[sample_no_perspective]]、centroid→[[center_perspective]](改变采样行为而非拼写),WGSL 丢弃centroid/sample并把linear→@interpolate(linear);钉住即固化 bug |
StructuredBuffer的Std140DataLayout/Std430DataLayout/ScalarDataLayout→ GLSL 块布局限定符 | hlsl、metal、wgsl | 无法表达:三者直接以 E36107(入口点含不可用特性,提示使用了Std140DataLayout)拒绝整个程序,无文本可查 |
-track-liveness在 GLSL 目标生成spirv_instruction函数 | hlsl、metal、spirv-asm、wgsl | 混合:HLSL/Metal/WGSL 在-track-liveness下不发射任何标记(无对应形式);spirv-asm 疑似错误——同一旗标令其发射 5 词OpLifetimeStart(语法只允许 3 词)且 slangc 退出码 255 |
27 个[format]属性各自选中的 SPIR-V 图像格式名 | hlsl、metal、wgsl | 混合:HLSL/Metal 从声明中完全丢弃格式(无可断言);WGSL 疑似错误——27 个格式中 19 个无拼写,一律替换为浮点格式rgba32float(整数格式也不例外),使textureLoad向i32/u32变量产出f32 |
无[format]的unorm/snorm存储图像按组件数推断 8 位 SPIR-V 格式 | glsl、hlsl、metal、wgsl | 混合:GLSL 疑似错误——unorm float4与snorm float4折叠到同一layout(rgba32f)(SPIR-V 保持区分);HLSL 无存储图像格式、把修饰符往返到元素类型上(RWTexture2D<unorm float>),Metal 连修饰符带格式一起丢弃,WGSL 以 E41400 拒绝声明 |
所有 "Absent targets" 行都同时列出哪些目标没有能力表达该声明,这正是特性化测试对"不能表达"与"表达错了"两种状态的诚实区分:前者不测,后者不钉、改归档。
六、不可达缺口分析
死代码与不可达路径
README 的 "Unreachable gaps" 节对"为什么这些发射臂没有被测试"做了逐条 triage,多数结论是应从代码库移除的死代码,而非补测对象:
GLSLSourceEmitter::_emitGLSLByteAddressBuffer(slang-emit-glsl.cpp 第 457 行)对 GLSL 目标已死:slang-emit.cpp为 GLSL 无条件设置byteAddressBufferOptions.translateToStructuredBufferOps = true,IRByteAddressBufferTypeBase根本活不到tryEmitGlobalParamImpl。实测确认:ByteAddressBuffer无论只用Load/Store还是只用GetDimensions,都发射为buffer StructuredBuffer_uint_t_N { uint _data[]; },永远不会走进该函数写出的_S<n>块。候选删除而非补测。GLSLSourceEmitter::_requireFragmentShaderBarycentric(第 155 行)已被取代:两个调用点(第 3856、3904 行的PerVertex臂)对应的pervertex片段输入会被slang-ir-glsl-legalize改写为IRPerVertexDecoration,由第 3975 行直接调用_requireGLSLExtension("GLSL_EXT_fragment_shader_barycentric")处理,第 156–159 行永不执行。重复逻辑,候选删除。CLikeSourceEmitter::emitType及两个同胞(slang-emit-c-like.cpp 第 655、673、684 行)在source/下无任何调用者:纯死代码。emitInterface/emitRTTIObject(第 584、591 行)是 no-op 基类桩:唯一真实实现是CPPSourceEmitter的覆盖;基类体只有在某目标不覆盖却把IRInterfaceType/IRRTTIObject送到发射时才运行,CLI 输入无法产生。emitLivenessImpl(第 690 行)经-track-liveness不可达:Khronos 目标上applyGLSLLiveness(slang-emit.cpp中的 pass,门控于isKhronosTarget)在发射前把标记改写为内建调用——这正是glsl-track-liveness-spirv-intrinsics.slang钉住的行为;而cpp/cuda/hlsl上同一旗标(带或不带-O1)根本不发射SLANG_LIVE_START/SLANG_LIVE_END。emitStringLiteral的转义开关(第 800–828 行)实际不可达:该函数只为 Torch/PyTorch 函数名与 HLSL decoration 名服务,这两者都不含制表符/换行/引号/反斜杠;printf格式串不经过它——-target cpp下含\t、\n、\"、\\、\r的printf能正确发射,同时让这些转义行全部保持未执行。getSortedWitnessTableEntries(第 371 行)只被CPPSourceEmitter::_emitWitnessTableDefinitions调用:ConstantBuffer<IShape>带两次-conformance注册编译到-target cpp时并不发射任何 witness table 定义,普通计算 shader 到不了它。- 防御性诊断臂("unhandled sampler state flavor"、"structured buffer type used unexpectedly"、
_emitGLSLTypePrefix的 "unhandled GLSL type prefix" 等)断言的是合法 CLI 输入无法产生的状态,不做目标。 IntPtrType/UIntPtrType臂(第 1105、1137、3569、3582 行)需要一个元素类型为指针宽整数的存储图像或 GLSL 类型,任何表面语法都构造不出来。
已归档的"疑似错误输出"与处理原则
对"当前输出被判定错误"的路径,测试束的原则是归档 finding 而非钉住错误输出(钉住会让将来修 bug 显得像回归)。束内记录了 7 条:
- Metal 插值修饰符:
_getInterpolationModifierText(slang-emit-metal.cpp 第 1726 行)把linear→[[sample_no_perspective]]、centroid→[[center_perspective]],改变采样行为而非拼写; - WGSL 插值修饰符:
emitInterpolationModifiersImpl(slang-emit-wgsl.cpp 第 1842 行)在缺少插值类型时完全丢弃centroid/sample,并把linear映射到非透视的@interpolate(linear); - GLSL
unorm/snorm存储图像格式:GLSL 路径把unorm float4与snorm float4都折叠到layout(rgba32f),而 SPIR-V 保持区分; -track-liveness在直接 SPIR-V 上产出无效模块:任何带被跟踪局部的 shader 都会发射 5 词的OpLifetimeStart(语法只允许 3 词),spirv-opt拒绝(slangc 退出 255、无输出)。根因是applyGLSLLiveness的门控isKhronosTarget对 SPIR-V 同样为真,但该 pass 把标记改写为只有 GLSL 文本路径消费的spirv_instruction调用。配套复现文件见 _repro/spirv-track-liveness-invalid.slang;- WGSL 用浮点格式顶替不支持的整数图像格式:27 个格式中 19 个无 WGSL 拼写,全部回退到
rgba32float并报 E31105 警告(整数格式也不例外),使RWTexture2D<int>变成texture_storage_2d<rgba32float, read_write>且textureLoad向i32变量产出f32;slang-emit-wgsl.cpp 第 477 行的default:臂直接返回字面量"rgba32float"而不查询格式的标量类别(尽管同一 switch 已拼出rgba32sint/rgba32uint)。配套复现文件见 _repro/wgsl-int-image-format-fallback.slang; - 跨函数边界返回
Optional<T*>在 SPIR-V 上永远编不完:探测SPIRVEmitContext::emitCastPtrToBool(slang-emit-spirv.cpp 第 9554 行)时发现——该函数是slang-ir-lower-optional-type.cpp产出的CastPtrToBool指令的降低实现,整个已发布套件仍未覆盖它,且挂起修复前无法覆盖; uniform接口类型全局加-conformance在所有目标上以内部错误中止,而同一程序的ConstantBuffer<IShape>写法可以编译。
此外还有两条"记录但未转成测试"的观察:GLSL mesh shader 在同时带out indices与out primitives参数时会重复发射layout(max_primitives = N) out;(glslang 接受、属于冗余而非非法,但钉住重复会让将来删除它显得像回归);以及"已提交测试未覆盖的目标"清单——glsl-image-format-inference-by-element-type.slang现在同时钉 SPIR-V、spirv-image-format-table.slang现在同时钉 GLSL、glsl-track-liveness-spirv-intrinsics.slang保持 GLSL-only。
七、漂移审查与可持续维护
README 的 "Drift review" 节展示了这套束的维护机制:在d592afa9b9提交处对照生成基准ef1068b548复查,期间有 37 个提交触及被监视路径,全部 13 个既有测试仍然通过。复查还处理了两种情况:
- 新诊断的处理原则:自基准提交以来,
coverage_targets新增三个诊断 E55210(abort格式串必须是字面量)、E57005(SPIR-V 资源堆步长对加速结构入口过小)、E57006(-spirv-resource-heap-stride与-spirv-unified-descriptor-heap-stride冲突)。三者都已有随引入它们的 PR 一起发布的手写回归测试(tests/spirv/descriptor-heap-acceleration-structure.slang、tests/spirv/descriptor-heap-unified-stride.slang、tests/diagnostics/abort-format-not-literal.slang),钉住的是同一可观察行为,因此不再重复添加测试——这避免了测试束与既有手工测试的冗余叠加; - 引文修正:
applyGLSLLiveness的调用点从:2603更正为slang-emit.cpp:2751(按本仓库当前源码为slang-emit.cpp中isKhronosTarget门控下的SLANG_PASS(applyGLSLLiveness)调用附近);CLikeSourceEmitter的各行号引文(:371、:584、:591、:690、:800)逐一复核后全部仍然精确。
这套"特性化 + 漂移复查 + 死代码/错误输出归档"的组合,让测试束在发射器持续演进时既保持钉住行为的能力,又不至于把 bug 或冗余固化进套件。
八、总结:这类测试束对编译器开发者的价值
coverage/emit 测试束提供的是一套可复制的后端测试方法论:
- 以"当前可观察输出"而非"理想输出"为断言对象,让长期无人触碰的发射臂先获得保护性覆盖;
- 用覆盖率数据驱动扫尾,但先怀疑 profile 本身不完整——重建参考 profile、只保留有增量贡献的候选,避免把测试做成噪声;
- 对"目标无法表达"与"目标表达错了"严格区分:前者不测、后者不钉而归档 finding,保证测试绿不是靠迁就 bug;
- 把死代码从"补测对象"中识别出来——
_emitGLSLByteAddressBuffer、emitType系列、emitInterface/emitRTTIObject桩、emitStringLiteral转义开关等都被证明不可达或可删除,测试束反过来成了代码清理的线索来源。
如果你正在维护 Slang 的任一目标后端,或想为自己的编译器建立类似的后端保护网,可以直接从 docs/generated/tests/coverage/emit/ 下的 13 个.slang测试及其 README 起步:它们既是可运行的测试,也是一份逐臂标注"为什么这个发射形态长这样"的活文档。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考