Slang 编译器诊断系统深入解析:DiagnosticSink、Lua 驱动诊断定义与富诊断渲染
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
本文聚焦 Shader 编译器 Slang 内部贯穿所有编译管线阶段的诊断(Diagnostics)系统:从中央接收接口DiagnosticSink,到以 Lua 表声明诊断、构建期生成 C++ 枚举与消息表的完整链路,再到严重级别、源码位置渲染、错误码命名空间与内部编译器错误(ICE)处理。读者读完可掌握"如何新增一条诊断"、"如何把 Slang 集成进消费其诊断的工具链"以及"如何定制错误格式化"所需的全部知识。
DiagnosticSink:所有管线阶段共用的诊断中枢
Slang 编译器从词法/预处理、解析、语义检查、AST 到 IR 的降级,直到最终代码生成,每个阶段都可能产生错误(error)、警告(warning)与附注(note)。这些信息统一汇入一个中心接口DiagnosticSink,其声明位于 source/compiler-core/slang-diagnostic-sink.h。
子系统的 sink 获取与贯穿方式
从源码结构看,各编译子系统通过以下方式获得 sink:
- 编译请求(编译入口)持有自己的 sink,贯穿到解析器、语义检查器、IR 优化等各级对象;
- sink 之间可以通过
setParentSink()/getParentSink()(slang-diagnostic-sink.h)建立父子关系,子 sink 会把格式化后的诊断转发给父 sink; - 新创建的 sink 可以从父 sink 拷贝显示与设置状态(flags、颜色模式、Unicode、已启用的警告分组、严重级别覆盖表),见构造函数 slang-diagnostic-sink.h。
sink 的输出有两种去向:若设置了ISlangWriter* writer,诊断写入该 writer;否则写入内部的StringBuilder outputBuffer(slang-diagnostic-sink.h)。工具可通过getBlobIfNeeded()把累积的诊断取为 blob,getErrorCount()则随时给出当前累计错误数,编译流程常用它做提前退出或最终成败判定。
diagnose 模板与参数打印
sink->diagnose(pos, info, args...)是核心入口(模板版本见 slang-diagnostic-sink.h)。它把参数打包成DiagnosticArg数组,并依据是否设置AlwaysGenerateRichDiagnostics标志分流到旧式diagnoseImpl或富诊断diagnoseRichImpl。参数通过一组printDiagnosticArg重载完成格式化,覆盖int32/uint32/int64/uint64/double、String、UnownedStringSlice、Name*、TokenType、Token、IRInst*、Modifier*等类型(slang-diagnostic-sink.h),因此诊断消息里可以安全地嵌入各类编译器对象。
此外,diagnoseRaw(severity, message)用于直接以纯文本追加诊断(典型场景是转发下游编译器报错),diagnoseWithoutSourceView()则用于给已有诊断追加 note 时避免重复打印同一行源码。
行为控制 Flags
sink->setFlag/resetFlag/isFlagSet控制的Flags位(slang-diagnostic-sink.h)包括:
| Flag | 作用 |
|---|---|
VerbosePath | 显示更详细(规范/绝对)路径 |
SourceLocationLine | 有源码时显示定位行 |
HumaneLoc | 显示 file/line 形式的人类可读位置 |
TreatWarningsAsErrors | 把(覆盖后的)警告提升为错误 |
LanguageServer | 以适合语言服务器的格式输出 |
AlwaysGenerateRichDiagnostics | 强制旧式诊断走新式富诊断路径 |
MachineReadableDiagnostics | 以机器可读 TSV 格式输出 |
另有若干渲染选项:setSourceLineMaxLength(默认 120 字符,0 表示不限)、setDiagnosticColorMode(SLANG_DIAGNOSTIC_COLOR_AUTO/ALWAYS/NEVER,AUTO 时依据 writer 是否控制台决定是否着色)、setEnableUnicode(未显式设置时按控制台输出自动探测)。
诊断定义:Lua 表驱动的声明式体系
Slang 的全部诊断集中声明在 Lua 文件中,构建期经代码生成转为 C++ 结构。主文件是 source/slang/slang-diagnostics.lua(约 6000+ 行),按错误码段组织(0xxxx命令行/宿主平台、15xxx预处理、2xxxx解析、3xxxx语义等);另有独立子文件,如类型错误集中放在 source/slang/diagnostics/type-errors.lua。
表结构(schema)
每条诊断在 Lua 中用辅助函数声明,核心字段为:
- name:唯一小驼峰/连字符名称,如
cannot-open-file、function-redefinition; - code(id):整数错误码,构建期映射为 C++ 枚举值;
- severity:
error、warning、note、internal、fatal之一; - message:消息模板,其中
~param为插值参数。
以 slang-diagnostics.lua 头部注释中的示例为准:
err( "function return type mismatch", 30007, "expression type ~expression.type does not match function's return type ~returnType:Type", span({loc = "expression:Expr", message = "expression type"}), span({loc = "function:Decl", message = "function return type"}) )插值语法有三类:
~param:默认 String 参数;~param:Type:带类型的参数(Type、Decl、Expr、Stmt、Val、Name、int等);~param.member:成员访问,如~expression.type会自动生成expression->type;Decl直接插值时自动使用.name。
位置参数语法为"location"(普通SourceLoc变量)或"location:Type"(类型化位置,如Decl->getNameLoc()、Expr->loc),类型化位置也可作为插值参数复用。
辅助函数:span、note 与变长结构
span(loc, message?):标记一个主/次跨度,message可选,默认为空;note(message, span...):附注,必须至少带一个 span,第一个为主跨度、其余为次跨度,不可嵌套;variadic_span(struct_name, loc, message):生成嵌套结构体与List<Error> errors形式的变长跨度列表(见"multiple type errors"示例);variadic_note(struct_name, message, span...):变长附注,如候选列表List<Candidate> candidates;standalone_note:无主诊断的独立 note(如编译耗时提示)。
位置函数err(name, code, message, [primary_span], ...)与warning(...)的 primary_span 是可选的——无源码位置的诊断(如命令行参数错误)可以不挂跨度。
构建期代码生成
整个链路记录在 slang-diagnostics.lua 头部注释:
- slang-diagnostics.lua 用
err()/warning()等辅助函数定义诊断; - source/slang/slang-diagnostics-helpers.lua 处理这些定义:先提取
span()/note()中的位置,再从~插值提取参数(自动去重),成员访问按位置类型解析; - slang-rich-diagnostics.h.lua 加载处理结果,FIDDLE 模板在 slang-rich-diagnostics.h 中生成每个诊断的 C++ 结构体(直接参数成为成员变量,位置成为
SourceLoc或类型化指针),并在slang-rich-diagnostics.cpp中生成toGenericDiagnostic():展开成员访问、插值参数拼装消息、设置主跨度/次跨度与附注。
消费侧头文件是 source/slang/slang-diagnostics.h,它明确注释"所有诊断现在都由slang-diagnostics.lua定义,并经slang-rich-diagnostics.h生成,旧的slang-diagnostic-defs.h已移除"。该头文件还导出findDiagnosticByName()、getDiagnosticsLookup()与overrideDiagnostic(s),供按名称查询和覆盖诊断严重级别使用。运行时查找由DiagnosticsLookup承担(slang-diagnostic-sink.h):按精确/宽松名称查找、按 id 查找(允许同 id 多条,返回先加入者)、addAlias别名注册,并用MemoryArena管理名称生命周期。
新一代声明式语法
source/slang/diagnostics/type-errors.lua 展示了一种更结构化的声明形式("guinea pig"原型诊断),以diagnostic "..."块描述完整结构:
diagnostic "argument_type_mismatch" { code = "E30019", severity = "error", flag = "type-mismatch", message = "cannot convert argument of type `{found}` to parameter of type `{expected}`", params = { { name = "func_name", type = "String" }, { name = "param_name", type = "String" }, { name = "param_index", type = "int" }, { name = "expected", type = "Type" }, { name = "found", type = "Type" }, }, primary_label = { loc = "arg_loc", message = "expected `{expected}`, found `{found}`", }, secondary_labels = { { loc = "param_loc", message = "parameter `{param_name}` declared as `{expected}` here" }, { loc = "func_loc", message = "in call to function `{func_name}`" }, }, notes = { "no implicit conversion exists from `{found}` to `{expected}`" }, helps = { "add explicit cast: `({expected}){found_expr}`" }, }与旧式err(...)相比,它显式区分主标签、次标签、notes 与 helps,语义更接近"面向 IDE 与 LLM 消费"的富诊断模型,也印证了富诊断的内部表示(GenericDiagnostic、toGenericDiagnostic()、getInfo(),见 slang-rich-diagnostics.h)。
严重级别
诊断系统的严重级别在Severity枚举中定义(slang-diagnostic-sink.h),并通过static_assert与公开 API 的SLANG_SEVERITY_*常量保持同步(slang-diagnostic-sink.h):
| 级别 | 名称(getSeverityName输出) | 语义 |
|---|---|---|
Disable | ignored | 被禁用/覆盖掉的诊断 |
Note | note | 附注,提供上下文 |
Warning | warning | 可编译但有隐患 |
Error | error | 常规编译错误 |
Fatal | fatal error | 致命错误,通常直接终止 |
Internal | internal error | 内部编译器错误(ICE) |
Lua 侧与之对应:helper 的add_diagnostic校验 severity 只能是error、warning、note、internal、fatal之一(slang-diagnostics-helpers.lua),并提供internal()/fatal()便捷函数;负数或特殊码(如-1、99999internal-compiler-error catch-all)是内部哨兵,不参与编号校验。
警告分组与覆盖
警告还带有WarningLevel分组(仿 clang/gcc 的-Wall/-Wextra/-Wpedantic),分组相互独立、非嵌套(slang-diagnostic-sink.h):
Default:始终输出,无需开启;Extra:默认开启(m_enabledWarningLevels初始只置该位,slang-diagnostic-sink.h);All、Pedantic:默认关闭,需显式enableWarningLevel()开启。
enableWarningLevel()对位索引做边界检查防止越界移位(slang-diagnostic-sink.h);overrideDiagnosticSeverity(id, severity, info)支持按诊断 id 单独覆盖(例如-Wno-xxx抑制、-werror提升),覆盖表m_severityOverrides可按get/setSeverityOverrides在父子 sink 间拷贝。
源码位置与消息渲染
诊断必须能定位到file:line:column。Slang 用SourceLoc表示抽象源码位置(声明于 source/compiler-core/slang-source-loc.h),由SourceManager负责映射:SourceManager::getHumaneLoc(loc, type)返回HumaneSourceLoc(slang-source-loc.h),把名义位置(SourceLocType::Nominal)解析为文件路径、行、列。
sink通过setSourceManager()绑定SourceManager(slang-diagnostic-sink.h),渲染时结合SourceLocationLexer(可选回调,用于对定位行的 token 做词法高亮,未设置时只在SourceLoc处显示单字符 caret)与SourceLineMaxLength等选项输出类似 clang 风格的行内定位。设置Flag::SourceLocationLine会显示带 caret 的源码行;Flag::HumaneLoc控制是否显示 file/line 人类可读位置。
富诊断是更完整的渲染层,声明于 source/slang/slang-rich-diagnostics.h:每个生成的诊断结构体通过toGenericDiagnostic()转成GenericDiagnostic,携带主跨度、次跨度、notes、helps 与消息;sink->diagnoseRichImpl()依据覆盖表与位置做最终渲染,并支持颜色(SlangDiagnosticColor)、Unicode(shouldEnableUnicode()与setEnableUnicode())、源行长度上限等选项。SLANG_INTERNAL_ERROR/SLANG_UNIMPLEMENTED/SLANG_DIAGNOSE_UNEXPECTED宏(slang-diagnostics.h)直接调用富诊断:debug 构建下先在诊断前追加note: internal error triggered at 文件:行的原始 note(因为主诊断可能中止编译),再输出Diagnostics::InternalCompilerError等富诊断对象。
错误码命名空间
诊断 id 的来源即 slang-diagnostics.lua 各条目的code字段,构建期生成 C++ 枚举(Diagnostics::*)与消息表,同一 id 可被DiagnosticsLookup::getDiagnosticById反查。代码按段分配命名空间:0xxxx命令行与宿主平台交互、15xxx预处理、2xxxx解析、3xxxx语义/类型,子段内再细分(如150xx条件、151xx指令解析、153xxinclude、154xx宏定义、155xx宏展开、156xxpragma)。
关于诊断的书写规范与文档化约定,仓库提供了 docs/diagnostic-guidelines.md:完整诊断采用error[E00000]: 主消息+--> file.slang:LL:CC+ 源码片段 + 主/次标签 += note/= help的 Rust/Clang 风格结构;错误码建议字母前缀加 5 位数字(如E0308、W00001);消息要求小写开头、无句末标点、代码元素用反引号、主动语态与牛津逗号;并提供-error-format=json、-show-error-codes、-explain E00001、-max-errors=N、-show-type-aliases=always|helpful|never、-color=auto|always|never、-verbose-diagnostics等命令行选项说明,以及错误级联抑制、优先级排序(语法错误 > 模块错误 > 类型定义 > 接口实现 > 类型不匹配 > 其他语义 > 警告 > remark)与 IDE/LSP 集成(DiagnosticRelatedInformation、CodeAction、severity 映射)的指导。
内部编译器错误(ICE)
编译器自身的断言失败会以Severity::Internal(internal error)上报,最终落到 catch-all 诊断internal-compiler-error(Lua 侧码99999)。
断言宏与 sink 的交互
核心断言宏定义在 source/core/slang-common.h:
SLANG_ASSERT(VALUE):debug 构建(_DEBUG)下调用::Slang::handleAssert(#VALUE, __FILE__, __LINE__, false);release 构建下退化为SLANG_ASSUME(VALUE)(对编译器提示假设,不执行检查);SLANG_RELEASE_ASSERT(VALUE):任何构建下都调用handleAssert(..., true)。
SLANG_ASSERT_FAILURE(msg)宏则定义在 source/core/slang-signal.h,同样转发到handleAssert。
handleAssert的实现位于 source/core/slang-signal.cpp,其关键行为:
- 读取环境变量
SLANG_ASSERT(getenv("SLANG_ASSERT"),见 slang-signal.cpp)来调节行为;读取时刻意不构建StringBuilder,避免断言输出路径内部再次触发断言导致重入; - 依据
SLANG_ASSERT的值决定是触发调试器断点、打印失败消息还是直接exit(-1)(slang-signal.cpp)。
与 sink 的正式衔接则是通过 source/slang/slang-diagnostics.h 的宏完成:SLANG_INTERNAL_ERROR(sink, pos)在 debug 下先diagnoseRaw(Severity::Note, "note: internal error triggered at 文件:行"),再sink->diagnose(Diagnostics::InternalCompilerError{...});release 下只输出富诊断。SLANG_UNIMPLEMENTED(sink, pos, what)、SLANG_DIAGNOSE_UNEXPECTED(sink, pos, message)结构相同。从源码结构看,SLANG_INTERNAL_ERROR等宏要求包含 slang-rich-diagnostics.h 以获得完整的Diagnostics::*结构体定义。
新增一个诊断:操作检查清单
在 Slang 中新增诊断的标准路径(结合 slang-diagnostics.lua 头部示例与 docs/diagnostic-guidelines.md 的最佳实践):
- 选定 Lua 文件与编号段:通用诊断写入 source/slang/slang-diagnostics.lua,类型相关诊断写入 source/slang/diagnostics/type-errors.lua;从对应错误码段(如
3xxxx)取未占用的 id,避免与既有码冲突(可用DiagnosticsLookup::getDiagnosticById验证); - 写消息文本:小写开头、无句末标点、代码元素用反引号;通过
~param、~param:Type、~param.member描述参数;Decl直接插值会自动取.name; - 声明参数与位置:能用
span({loc = "expr:Expr", message = "..."})声明主跨度,用note(...)/variadic_span/variadic_note补充次跨度与附注;primary_span 可选(无位置的命令行类诊断可省略); - 选择严重级别:
err()或warning();需要分组管控的警告可传extra/pedantic哨兵使其默认受-W组开关控制;特殊场景用internal()/fatal()/standalone_note(); - 在合适位置调用 sink:找到持有
DiagnosticSink的编译站点,调用sink->diagnose(pos, Diagnostics::YourDiagnostic{...})(富诊断结构体)或sink->diagnose(pos, info, args...)(旧式路径);无源码位置的用diagnoseRaw; - 考虑覆盖与级联:如需支持
-Wno-xxx抑制,确保诊断注册进DiagnosticsLookup(富诊断由getRichDiagnosticsInfo()自动注册);遵循错误级联抑制原则,标记受污染符号、限制传播; - 补测试:按 docs/diagnostic-guidelines.md 的约定为诊断编写至少一个测试(正例与负例),验证消息文本、跨度与 fix-it 建议。
延伸阅读
- docs/diagnostic-guidelines.md:诊断消息书写规范、错误码格式、JSON 输出与 IDE 集成约定;
- source/compiler-core/slang-diagnostic-sink.h:sink 完整接口、
DiagnosticInfo、DiagnosticsLookup; - source/slang/slang-diagnostics.lua 与 source/slang/slang-diagnostics-helpers.lua:诊断定义 DSL 与构建期处理逻辑;
- source/slang/slang-rich-diagnostics.h:富诊断结构体的 FIDDLE 生成模板与消费接口;
- source/core/slang-common.h、source/core/slang-signal.cpp:断言与 ICE 上报的底层实现。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考