Slang 编译流水线之语义检查:从裸 AST 到类型完备可降级 IR 的完整实现解析
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
导读
本文以 Slang 开源编译器(GitHub_Trending/sl/slang)官方设计文档 docs/generated/design/pipeline/03-semantic-check.md 为核心骨架,系统讲解流水线第三阶段——语义检查(Semantic Checking):它把解析器产出的裸 AST 变成名字已解析、类型已附着、conformance 已记录、修饰符已验证、函数体已完整检查的 AST,为 AST → IR 降级做好准备。读完本文,你将掌握SemanticsVisitor一族访问器的文件分工与调用链、DeclRef名称解析机制、泛型约束求解与可微性 conformance 化实现、修饰符冲突组与可见性作用域规则、着色器入口点专项检查,以及错误恢复与诊断的工程细节。
阶段定位:输入、输出与前后衔接
语义检查位于编译流水线的第三环,承接 02-parse-ast.md(解析成 AST),输出交给 04-ast-to-ir.md(AST → IR 降级)。
输入:解析器产生的 AST,其中函数体仍以UnparsedStmt形式保存(尚未解析成语句树),基类列表仍是未检查的Expr,没有任何表达式携带类型。
输出:同一棵 AST,但满足六项不变量:
- 名字已解析(names resolved)——每个携带
DeclRef的节点都指向规范(canonical)声明; - 类型已附着(types attached)——每个
Expr都携带一个Type*; - conformance 已记录(conformances recorded)——类型与接口的 witness 表已构建;
- 修饰符已验证(modifiers validated)——非法组合与错误位置被拒绝;
- 默认 conformance witness 已合成(default conformance witnesses synthesized)——接口未显式实现的成员由编译器补全;
- 函数体已完整解析并检查(function bodies fully parsed and checked)——
UnparsedStmt变成已检查的Stmt树。
文档用一个最小示例完整演示了这六项:
interface IFoo { int base(); int twice() { return base() * 2; } } struct S : IFoo { int base() { return 6; } static const int tag = 1; } int call<T : IFoo>(T v) { return v.twice(); }- 检查器把
S基类列表中的IFoo与T约束中的IFoo解析到同一个声明(names resolved); - 给
v.twice()附着类型int,从而使该调用合法(types attached); - 记录
S : IFoo的 witness 表(conformances recorded); - 由于
S没有显式实现twice,检查器把接口默认函数体作为 witness 填入该表(default conformance witnesses synthesized); - 把
call的UnparsedStmt函数体解析并检查成Stmt树(function bodies fully parsed and checked); - 对
S::tag上的static,checkModifier通过isModifierAllowedOnDecl确认 struct 字段可挂static,并通过getModifierConflictGroupKind确认static的互斥组尚未被占用(modifiers validated)——假如同时写了uniform,就会触发冲突诊断。
SemanticsVisitor:检查器的架构骨架
语义检查器实现为一族共享SemanticsContext状态的访问器子类。基访问器声明在 slang-check-impl.h(第 1778 行):
struct SemanticsVisitor : public SemanticsContext顶层入口是 slang-check.cpp 中的checkTranslationUnit(第 181 行),前端在解析收集完 decls 后,对每个TranslationUnitRequest调用一次。其核心流程(可从源码直接看到):
- 构造
SharedSemanticsContext,绑定 linkage、module、诊断 sink、已加载模块字典与 translation unit; - 用该上下文实例化
SemanticsDeclVisitorBase; - 调用
visitor.checkModule(translationUnit->getModuleDecl())对翻译单元内所有声明执行主检查; - 调用
_collectShaderParams收集着色器参数。
SemanticsVisitor还提供了dispatchStmt/dispatchExpr两个分发入口,内部实例化SemanticsStmtVisitor/SemanticsExprVisitor并统一捕获异常,保证出错时向DiagnosticSink记录内部错误位置。
slang-check-*.cpp家族:按关注点拆分
source/slang/下的slang-check-*.cpp一族文件按职责拆分检查工作,全部通过 slang-check-impl.h 中的SemanticsContext/SemanticsVisitor协作。下表同时给出每个文件自身会发射的代表性诊断,使每一行都成为可写测试的断言目标:
| 文件 | 关注点 | 代表性诊断(Example rejection) |
|---|---|---|
| slang-check.cpp | 入口点;编排各检查阶段 | 无——它只负责阶段排序,自身诊断仅关于下游编译器加载 |
| slang-check-decl.cpp | Decl检查——类型、签名、默认值、属性 | E30200:声明与更早的声明冲突 |
| slang-check-expr.cpp | Expr检查——类型推断、左值性、转换 | E30011:对非左值赋值 |
| slang-check-stmt.cpp | Stmt检查——控制流、作用域规则、返回类型验证 | E30003:break出现在循环或switch之外 |
| slang-check-type.cpp | 解析以Expr形式出现的Type引用 | E30060:表达式被用在需要类型的位置 |
| slang-check-overload.cpp | 重载解析;对 lookup 产出的候选排序 | E40018:指出拒绝某候选的具体实参的 note |
| slang-check-conformance.cpp | 验证与合成接口 conformance | 无——缺失要求的E38100由其在slang-check-decl.cpp中的调用方报告 |
| slang-check-conversion.cpp | 隐式转换排序与强制转换点检查 | E30523:初始化列表中初始值过多 |
| slang-check-inheritance.cpp | 继承与 extension 查找;facet 计算 | E30815:循环extension(见下文精确语义) |
| slang-check-modifier.cpp | 修饰符组合与属性参数验证 | E31202:一个 decl 上同一互斥组出现两个修饰符 |
| slang-check-constraint.cpp | 泛型约束求解(where子句、witness 推断) | E30433:pack 数量不满足countof(...)约束 |
| slang-check-resolve-val.cpp | 解析并规范化Type、DeclRef、witness 值 | 无——坏的解析结果在使用处报告 |
| slang-check-shader.cpp | 入口点检查——阶段特定签名、参数规则 | E38007:没有 stage 的入口点 |
E30815的精确语义:重入检测而非通用环检测
表里的E30815比"循环 extension"字面含义更窄,很容易被过度解读。它不是对 extension 目标类型做通用环检测,只会在同一个ExtensionDecl在其继承信息仍计算期间被重入时触发。
getInheritanceInfo(DeclRef<ExtensionDecl>)会把命名该 extension 的InheritanceCircularityInfo节点压入链表并递归;_checkForCircularityInExtensionTargetType(slang-check-inheritance.cpp 第 254 行)进入时遍历该链表,若发现相同的Decl*已存在,就报告CircularityInExtension并返回空的InheritanceInfo,使递归终止而非栈溢出。
因此,那些从不重入同一个 extension的环属于其他诊断:读者最容易想到的几种形态——互相引用的 extension 目标类型、extension 内自引用的typealias、通过This到达的 extension——分别报告E30027、E30813或致命的E40002cyclic reference。紧邻其上还有一个刻意为之的良性场景:_isInheritanceInfoBeingComputed的存在使__constraint A == B这类让T.A与T.B互为彼此的基类的相等约束,在线性化时被跳过而非报告为环。
与解析器的两阶段交互
解析器把函数与方法体保留为UnparsedStmt节点。检查器遇到时调用parseUnparsedStmt(slang-parser.h),并传入SemanticsVisitor*,使解析器能在解析期间回调检查器来消歧<记号(泛型实参 vs 比较运算符)。函数体解析完毕后,检查器继续在产生的Stmt树上正常推进。
这意味着函数体内部不存在干净的 parse/check 边界:解析与检查按需同步进行。更深入的理由见 docs/design/parsing.md。
名称查找与DeclRef
名称解析产生DeclRef——一个声明加上记录其泛型参数与外围上下文参数如何绑定的替换(substitution)。具体的DeclRefBase运算(DirectDeclRef、LookupDeclRef、替换应用)实现在 slang-ast-decl-ref.cpp。算法层面的规则——作用域构造、查找算法、遮蔽、可见性过滤、重载解析——位于 docs/generated/design/name-resolution/ 子目录,建议从 index.md 读起(其下还有lookup.md、overload-resolution.md、scopes.md、visibility.md四篇)。DeclRef本身的设计动机见 docs/design/decl-refs.md。
泛型特化与约束
检查器通过三个文件协作实现泛型参数解析:
- slang-check-constraint.cpp——累积并求解类型/值/witness 约束;
- slang-check-conformance.cpp——寻找(或合成)类型满足约束所需接口的 witness;
- slang-check-resolve-val.cpp——泛型解析后验证
Val替换。
泛型应用的约束求解路径
解析泛型应用时,TryCheckOverloadCandidateConstraints(slang-check-overload.cpp)把最外层泛型的默认实参与 witness 实参送入约束求解器的不动点(trySolveGenericArguments)——与推断实参走同一路径——只把显式提供的普通实参前缀(OverloadCandidate::explicitGenericArgCount,定义于 slang-check-impl.h)作为固定的调用方输入,避免用户手写的自引用实参被参数的默认值覆盖。求解失败时,代码回退到逐约束线性扫描,重新推导失败的约束以产生精确诊断。
关联类型约束的统一表示
写在关联类型上的约束——无论是associatedtype A : IBar、associatedtype A where A : IBar还是__constraint A : IBar——都被统一记录为外围接口的GenericTypeConstraintDecl需求(是A的兄弟节点,而不是嵌套在A之下)。在这种统一表示下,findWitnessForInterfaceRequirement(slang-check-decl.cpp)满足接口级约束需求的方式是:在This被替换为具体 conforming 类型后,重新检查子类型(或对==约束检查类型相等)关系,而不是去该类型上找成员。conformance 合成已经安装的 witness——例如enum合成的__Tag : __BuiltinIntegerType(包括bool标记情形,此时不存在真正的子类型 witness,用NoneWitness标记编译器信任的约束已满足)——会被该函数顶部的 witness 表早退(early-out)直接采信。
继承列表线性化与良性环
线性化继承列表由getInheritanceInfo/_calcInheritanceInfo(slang-check-inheritance.cpp)计算。计算T.D这类关联类型访问的继承时,引擎会:浮出每个锚点类型所 conforms 接口的接口级__constraint,通过锚点的 conformance witness 重新表达每条约束,并把相反端点作为该访问的基类加入。__constraint A == B这类相等约束使T.A与T.B互为基类——一个良性环。引擎通过跳过继承信息仍在计算中的基类(_isInheritanceInfoBeingComputed)来容忍它,把被跳过的进行中祖先DeclRef累积到HashSet<DeclRef<Decl>>* ioSkippedIncompleteFacet输出参数中;某帧在减去自身后若跳过集合非空,则其结果是上下文相关(部分)的,不缓存,由后续根级查询重算。裸This出现在接口__constraint主语位置(表达继承而非被检查的谓词)会在visitGenericTypeConstraintDecl(slang-check-decl.cpp)检查期间被拒绝。
特化失败:急切记录、惰性报告
当泛型无法为某调用特化时,失败原因被急切捕获但惰性报告。约束求解器记录一个GenericArgumentInferenceFailure(slang-check-impl.h)——一个 tagged union,其Kind同时决定存储的载荷与最终发射的诊断:
Kind | 诊断 | 触发示例 |
|---|---|---|
VariadicPackCountMismatch | E30433 | takesTwo(1, 2, 3)对void takesTwo<each T>(expand each T args) where countof(T) == 2 |
GenericArityMismatch | E30438 | 实参列表完全无法匹配泛型形参列表的调用 |
OrdinaryGenericParamNotInferred | E30439 | f(1)对void f<T>(int a)——没有任何实参提及T |
InterfaceConformanceNotSatisfied | E38029 | pick(s)对T pick<T : IFoo>(T a),而S未实现IFoo |
GenericConstraintNotSatisfied | E30440 | f(1.5f)对void f<T>(T a) where T == int;是所有非 conformance 约束的回退项 |
GenericParamUnificationConflict | E30442 | two(x, y)对void two<T>(T a, T b),而A、B不相关 |
每个分支在错误后都附一条携带候选渲染签名(GenericSignatureTried)的 "see declaration of" note;GenericConstraintNotSatisfied分支额外加一条指向where子句的 note。每个Kind只存储有问题的字段(数量、形参Decl*或替换后的子/超类型),昂贵的消息格式化被推迟,使投机性候选永远不必为格式化买单。失败被附着到OverloadCandidate上,仅当重载解析最终选中该失败候选时才转为聚焦的诊断——见CompleteOverloadCandidate(slang-check-overload.cpp)中对candidate.genericInferenceFailure.kind的switch。在此机制之前,所有特化失败都会坍缩成兜底诊断Diagnostics::GenericArgumentInferenceFailed。
可微性:作为接口 conformance 记录
可微性被记录为把函数视为类型后的接口 conformance,而非后续阶段再推导的修饰符事实。考虑:
[Differentiable] float f(float x) { return x * x; }[Differentiable]解析为BackwardDifferentiableAttribute(见 core.meta.slang 中attribute_syntax声明,source_commit对应行 470)。当SemanticsDeclHeaderVisitor::checkDifferentiableCallableCommon(slang-check-decl.cpp 第 15624 行)看到该属性或ForwardDifferentiableAttribute时:
- 调用
extendContainerDecl合成extension __func_as_type(f) : IForwardDifferentiable<__func_as_type(f)>; - 调用
addSynthesizedFunc给该 extension 加上接口要求的fwd_diff成员,以kIROp_ForwardDifferentiate作为其实现。
合成出的fwd_diff又被赋予同一对 conformance,这正是高阶微分能通过普通 lookup 解析的原因。
接口类型本身由getForwardDiffFuncInterfaceType/getBackwardDiffFuncInterfaceType(slang-check-decl.cpp 第 10051/10057 行)构建:把基础函数类型与__hasDiffTypeInfowitness 配对——后者正是IForwardDifferentiable<FType>/IBackwardDifferentiable<FType>的要求(这两个接口声明于 core.meta.slang 第 720/739 行,其需求fwd_diff、BwdCallable/MinimalContext关联类型、apply_bwd是检查器必须供给的)。
因为事实现在位于 witness 表,"该被调用者可微吗"变成子类型查询而非修饰符查找:isFuncForwardDifferentiable/isFuncBackwardDifferentiable(第 5467/5476 行)返回tryGetSubtypeWitness产生的SubtypeWitness*,取代了早先的布尔谓词doesCalleeHaveFwdDiff/doesCalleeHaveBwdDiff。返回 witness 而非bool很关键:调用方需要该 witness 来构建并特化导数调用。
[Differentiable]标注在接口需求上时,与前述关联类型约束同样处理——作为外围接口的需求而非嵌套在成员之下。_moveInterfaceDifferentiabilityRequirementToInterface(第 14863 行)先在拥有该类型提及的泛型环境的 callable 下创建GenericTypeConstraintDecl,再用liftDeclFromGenericContainers把它提升为接口下独立的泛型需求。显式拼写__func_extension fwd_diff(foo)(...)则通过_funcExtensionForwardDiff/_funcExtensionBackwardDiff(第 15981/16016 行)到达同一表示:改写为extension foo : IForwardDifferentiable<foo>,用户函数体即fwd_diff成员。
完整的概念模型(接口、witness 表、存在类型)见 docs/design/interfaces.md 与 docs/design/existential-types.md;本文只指向实现。
合成隐式代码
部分声明在检查期(而非解析期)获得成员:默认 conformance witness、生成的比较/构造方法、若干内建 conformance。"合成什么"的决策主要位于 slang-check-decl.cpp——例如_synthesizeCtorSignature(默认构造函数)、trySynthesize*RequirementWitness系列(接口需求)——而 slang-ast-synthesis.cpp 提供ASTSynthesizer助手(emitBinaryExpr、emitVarExpr、emitInvokeExpr、emitVarDeclStmt等)来构建这些例程发射的 AST 片段。只要检查器需要用户未写但语言保证存在的成员,就会调用这套机制。
修饰符验证
修饰符专用检查位于 slang-check-modifier.cpp:哪些修饰符可挂在哪些 decl 上、互斥组合、属性实参类型、以及 Slang 与 GLSL 输入之间的差异。修饰符节点本身定义于 slang-ast-modifier.h。
互斥组与冲突诊断
"互斥"由getModifierConflictGroupKind(slang-check-modifier.cpp 第 1564 行)决定:它把修饰符的ASTNodeType映射到其竞争的组;第二个落入某 decl 上已被占用组的修饰符产生E31202。多数修饰符自成一组,所以static static int g;这种单纯重复是最常见情形;但含多个成员的组值得记住:
out、inout、ref、borrow共享一组;static与uniform共享一组;nointerpolation、noperspective、linear、sample、centroid共享一组。
GLSL 方言轴:-allow-glsl
这里的方言轴是 GLSL 而非 HLSL。checkModifier(第 1936 行)从-allow-glsl选项(CompilerOptionName::AllowGLSL)或模块上的GLSLModuleModifier计算isGLSLInput,并把它传给isModifierAllowedOnDecl(第 1675 行);被该谓词拒绝的修饰符位置报告为E31201(modifier ' ' is not allowed here)。
一个能实际触达它的具体组合:函数参数上的globallycoherent:
void f(globallycoherent int x) { }未加-allow-glsl时,GloballyCoherentModifier与HLSLVolatileModifier分支(第 1731 行)要求as<VarDecl>(decl)——而参数是ParamDecl,它派生自VarDeclBase,是VarDecl的兄弟而非子类(见 slang-ast-decl.h 第 321、339、597 行)——谓词返回 false,修饰符被拒绝。这是一个解析器乐意产出、因此可达的位置;读者可能尝试的许多其他组合要么在解析器阶段被解决,要么被直接接受,根本到不了E31201。
同一个分支最能说明 GLSL 标志"放宽了什么、没放宽什么",因为两个分支很容易读反。两个分支都已经接受 struct 字段:非 GLSL 分支允许父节点是任意StructDecl的VarDecl,所以struct G { globallycoherent int a; }无论是否带-allow-glsl都被接受,该标志在此无可观察差异。isGLSLInput额外带来的是上述参数情形(as<ParamDecl>(decl))、是VarDeclBase而非VarDecl的全局声明,以及与既有分支冗余的"本身是全局的 struct 的字段"。实际上只有少量条目按该标志分支。
可见性作用域:类型作用域而非文件作用域
public、internal、private与其他修饰符一样被检查,但它们命名的作用域不是源文件。isDeclVisibleFromScope(slang-check-expr.cpp 第 1148 行)判定:
| 修饰符 | 可见于 |
|---|---|
public | 任意作用域,包括其他模块 |
internal | 声明所在模块内的任意作用域 |
private | 声明的类型或命名空间,以及该类型的 extensions |
因此private是类型作用域而非文件作用域:同一文件中的自由函数不能读取struct的private成员,读取会被E30600拒绝;在没有外围类型可供其限定作用域的位置写private(全局作用域,或接口需求上)会以E30603提前拒绝。
dyn interface限制
validateDynInterfaceUsage与validateDynInterfaceUseWithInheritanceDecl(slang-check-decl.cpp 第 372/442 行)约束dyn interface可声明的成员与可 conforms 它的类型。两者都由allowExperimentalDynamicDispatch(第 364 行)门控,只有在模块语言版本为 2026 或更高(-std 2026)且未传入-enable-experimental-dynamic-dispatch时才生效——因此同一份源码在默认-std下编译结果不同。
门打开时:
- 接口不得是泛型(
E33072),不得声明关联类型(E33073)、泛型方法(E33074)、[mutating]方法(E33075)、[Differentiable]方法(E33076)、或非dyn基接口(E33077); - conforming 类型不得通过
extension获得 conformance(E33078),不得是泛型(E33082),其字段既不能是 unsized(E33079)、opaque(E33080)也不能非可拷贝(E33081)——动态表示必须是一个可拷贝、定长的盒子。
着色器专用检查
slang-check-shader.cpp 验证入口点:函数的 stage 属性、参数修饰符(in、out、inout与阶段专用 intrinsic)、返回类型与 stage 的兼容性、资源绑定规则。失败以引用shader("...")属性或入口点签名的诊断呈现。
以下五项检查专门针对入口点验证(而非通用推断遍历),值得单独说明:
- 泛型结构体 capability 需求。
collectGenericStructTypeUses(slang-check-shader.cpp)递归遍历入口点签名类型,找出每个用户自定义泛型结构体(例如Foo<int>,包括嵌套在Optional<...>、数组或ConstantBuffer<...>内部的)并对其[require(...)]做目标校验。通用 capability 推断遍历(SemanticsDeclReferenceVisitor)只为DirectDeclRef记录类型需求;泛型特化是GenericAppDeclRef会被跳过,否则需求会被丢弃。该检查刻意放在这里而非推断遍历中,以免每个命名此类类型的库函数被迫重新声明 capability。带MagicTypeModifier/IntrinsicTypeModifier的内建泛型类型已有更具体的诊断,被过滤掉(但仍被穿过递归)。目标无法满足的需求——SPIR-V 入口点签名中Foo<int>引用[require(cpp)] struct Foo<T>——以E36107报告在入口点上,后随 "see using of 'Foo'" note。 - 未特化的泛型入口点。
void main<T>(...)这类真正未特化的泛型入口点会降级为IRGeneric而非IRFunc,过去会在链接期崩溃。createSpecializedGlobalAndEntryPointsComponentType(slang-check-shader.cpp)现在用Linkage::isSpecialized结合特化实参字符串是否存在来判断,只在真正未特化时调用diagnoseGenericEntryPoint(第 3964 行)对入口点名发射E38014。 - 冲突的深度输出。片段入口点最多写一个深度系统值。由于逐参数语义检查孤立地看待每个 semantic,冲突被单独检测:
collectDepthOutputSemantics(slang-check-shader.cpp,source_commit对应第 536 行)遍历每个out/inout参数与返回类型——解开Conditional<T>与数组包装、递归进入 struct 字段,因此out DepthOut a[1]字段上的 semantic 也能被触达——收集计数大于一即产生Diagnostics::MultipleDepthOutputSemantics(E30705),把第二个贡献者命名为与第一个冲突。 - 系统值 semantic 类型兼容性。
isSemanticTypeCompatible(第 112 行)决定声明的类型能否携带给定系统值 semantic。两类型形状相同(都是标量,或都是元素数相等的向量)且标量元素类型落在同一类别(整数、浮点、bool)即匹配。这允许int3携带uint3semantic 这类符号强转,同时拒绝跨类别者(float gi : SV_GroupIndex)与形状不匹配者(float pos : SV_Position);两者都报告为E30701,消息列出该 semantic 接受哪些类型。 - 入口点参数上被忽略的绑定修饰符。Slang 在某些位置静默忽略
[[vk::binding(...)]]、[[vk::push_constant]]、register()、packoffset(),无论忽略与否都误导用户。因此入口点参数检查对每个此类修饰符报告Diagnostics::UnhandledModOnEntryPointParameter(E38010,消息点名修饰符与参数并说明它将被忽略)。只有[[vk::binding(...)]]情形被门控——_allTargetsSupportVkBindingOnEntryPointParameters(第 1580 行)遍历 linkage 的全部目标、isVkBindingCompatibleEntryPointParameterType(第 920 行)检查参数自身类型——使它只在该属性真会被丢弃时触发。[[vk::push_constant]]、register()、packoffset()出现在入口点参数上时无条件诊断。
失败模式:错误恢复与高价值诊断
所有语义检查错误都流经线程进SemanticsContext的DiagnosticSink。检查级恢复的一般策略是"用占位类型继续",避免一个错误级联爆炸:未解析的声明变成ErrorType类型,重载解析返回合成的errorExpr而非中止。诊断尽力点名违规源码构造:当ExpectATypeRepr(slang-check-type.cpp)发现不表示类型的表达式时,用表达式的实际类型(以及可用时的被引用名)构建Diagnostics::ExpectedAType消息。
以下几项诊断超越了"点名构造",直接指向修复方向:
- 逐候选实参不匹配。调用匹配不到任何重载时,诊断现在列出每个候选签名及拒绝它的具体实参。
slang-check-overload.cpp在候选上记录违规实参索引与期望/实际类型,再对每个候选发射Diagnostics::OverloadCandidateArgumentTypeMismatchnote。以两个float调用void g(A, int)/void g(B, float):调用处报E39999,每个候选一条E40011candidate: <signature>note,各随一条E40018note:argument 0 does not match: expected 'A', got 'float'。候选按渲染签名字符串去重(而非按Decl*,否则会把foo<float>与foo<int>这类不同特化错误合并);最多打印十个唯一候选,其余由E40015"N more overload candidates" note 汇总。 - 未定义标识符的"Did you mean ...?"。名称解析失败时,
slang-check-expr.cpp遍历作用域内候选,通过StringUtil::calcLevenshteinDistanceCaseInsensitive把保守的相似名建议附着到现有Diagnostics::UndefinedIdentifier(E30015)上,而非发射游离 note。findClosestInScopeName(第 5327 行)把预算定得很具体:长度小于 3 或大于 256 的名字不给出任何建议;允许距离为min(3, max(1, length / 3))——大约每三个字符一次编辑,下限 1 上限 3;核心模块声明与作用域无法访问的内容被跳过;两个不同名字打成平手时抑制建议,使输出不依赖作用域遍历顺序。于是myLongVariableNam建议myLongVariableName,而ac不会建议作用域内的ab,sqr不会建议核心模块的sqrt。 - 被丢弃的
[NoDiscard]结果。maybeDiagnoseDiscardedNoDiscardResult(slang-check-stmt.cpp)在[NoDiscard]标记函数的调用结果被丢弃——f();写成裸表达式语句——时以E30059触发,递归穿过逗号、三元选择与短路形式以找到被丢弃的子表达式。裸丢弃的构造函数调用被刻意排除。
诊断基础设施整体见 docs/generated/design/cross-cutting/diagnostics.md。
收尾:checkModule与降级就绪
checkModule把翻译单元中每个Decl驱动经过DeclCheckState序列直至CapabilityChecked;没有单独的 errored 状态,因此恢复表现为"诊断 + 错误类型/错误表达式就地替换"。处理完毕的 AST 即可交给 IR 降级(见 04-ast-to-ir.md)。从整体流水线视角看,语义检查正是 docs/generated/design/pipeline/overview.md 所描绘的"parse → semantic check → lower → IR passes → emit"链条上承上启下的一环,其驱动文件即本文反复引用的slang-check.cpp与slang-check-*.cpp家族。
【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考