拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Slang 编译流水线之语义检查:从裸 AST 到类型完备可降级 IR 的完整实现解析

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.mdAST → 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 callT : 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上的staticcheckModifier通过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 rejectionslang-check.cpp入口点编排各检查阶段无——它只负责阶段排序自身诊断仅关于下游编译器加载slang-check-decl.cppDecl检查——类型、签名、默认值、属性E30200声明与更早的声明冲突slang-check-expr.cppExpr检查——类型推断、左值性、转换E30011对非左值赋值slang-check-stmt.cppStmt检查——控制流、作用域规则、返回类型验证E30003break出现在循环或switch之外slang-check-type.cpp解析以Expr形式出现的Type引用E30060表达式被用在需要类型的位置slang-check-overload.cpp重载解析对 lookup 产出的候选排序E40018指出拒绝某候选的具体实参的 noteslang-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 推断E30433pack 数量不满足countof(...)约束slang-check-resolve-val.cpp解析并规范化Type、DeclRef、witness 值无——坏的解析结果在使用处报告slang-check-shader.cpp入口点检查——阶段特定签名、参数规则E38007没有 stage 的入口点E30815的精确语义重入检测而非通用环检测表里的E30815比循环 extension字面含义更窄很容易被过度解读。它不是对 extension 目标类型做通用环检测只会在同一个ExtensionDecl在其继承信息仍计算期间被重入时触发。getInheritanceInfo(DeclRefExtensionDecl)会把命名该 extension 的InheritanceCircularityInfo节点压入链表并递归_checkForCircularityInExtensionTargetTypeslang-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节点。检查器遇到时调用parseUnparsedStmtslang-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——寻找或合成类型满足约束所需接口的 witnessslang-check-resolve-val.cpp——泛型解析后验证Val替换。泛型应用的约束求解路径解析泛型应用时TryCheckOverloadCandidateConstraintsslang-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之下。在这种统一表示下findWitnessForInterfaceRequirementslang-check-decl.cpp满足接口级约束需求的方式是在This被替换为具体 conforming 类型后重新检查子类型或对约束检查类型相等关系而不是去该类型上找成员。conformance 合成已经安装的 witness——例如enum合成的__Tag : __BuiltinIntegerType包括bool标记情形此时不存在真正的子类型 witness用NoneWitness标记编译器信任的约束已满足——会被该函数顶部的 witness 表早退early-out直接采信。继承列表线性化与良性环线性化继承列表由getInheritanceInfo/_calcInheritanceInfoslang-check-inheritance.cpp计算。计算T.D这类关联类型访问的继承时引擎会浮出每个锚点类型所 conforms 接口的接口级__constraint通过锚点的 conformance witness 重新表达每条约束并把相反端点作为该访问的基类加入。__constraint A B这类相等约束使T.A与T.B互为基类——一个良性环。引擎通过跳过继承信息仍在计算中的基类_isInheritanceInfoBeingComputed来容忍它把被跳过的进行中祖先DeclRef累积到HashSetDeclRefDecl* ioSkippedIncompleteFacet输出参数中某帧在减去自身后若跳过集合非空则其结果是上下文相关部分的不缓存由后续根级查询重算。裸This出现在接口__constraint主语位置表达继承而非被检查的谓词会在visitGenericTypeConstraintDeclslang-check-decl.cpp检查期间被拒绝。特化失败急切记录、惰性报告当泛型无法为某调用特化时失败原因被急切捕获但惰性报告。约束求解器记录一个GenericArgumentInferenceFailureslang-check-impl.h——一个 tagged union其Kind同时决定存储的载荷与最终发射的诊断Kind诊断触发示例VariadicPackCountMismatchE30433takesTwo(1, 2, 3)对void takesTwoeach T(expand each T args) where countof(T) 2GenericArityMismatchE30438实参列表完全无法匹配泛型形参列表的调用OrdinaryGenericParamNotInferredE30439f(1)对void fT(int a)——没有任何实参提及TInterfaceConformanceNotSatisfiedE38029pick(s)对T pickT : IFoo(T a)而S未实现IFooGenericConstraintNotSatisfiedE30440f(1.5f)对void fT(T a) where T int是所有非 conformance 约束的回退项GenericParamUnificationConflictE30442two(x, y)对void twoT(T a, T b)而A、B不相关每个分支在错误后都附一条携带候选渲染签名GenericSignatureTried的 see declaration of noteGenericConstraintNotSatisfied分支额外加一条指向where子句的 note。每个Kind只存储有问题的字段数量、形参Decl*或替换后的子/超类型昂贵的消息格式化被推迟使投机性候选永远不必为格式化买单。失败被附着到OverloadCandidate上仅当重载解析最终选中该失败候选时才转为聚焦的诊断——见CompleteOverloadCandidateslang-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::checkDifferentiableCallableCommonslang-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/getBackwardDiffFuncInterfaceTypeslang-check-decl.cpp 第 10051/10057 行构建把基础函数类型与__hasDiffTypeInfowitness 配对——后者正是IForwardDifferentiableFType/IBackwardDifferentiableFType的要求这两个接口声明于 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 : IForwardDifferentiablefoo用户函数体即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。互斥组与冲突诊断互斥由getModifierConflictGroupKindslang-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 行被该谓词拒绝的修饰符位置报告为E31201modifier is not allowed here。一个能实际触达它的具体组合函数参数上的globallycoherentvoid f(globallycoherent int x) { }未加-allow-glsl时GloballyCoherentModifier与HLSLVolatileModifier分支第 1731 行要求asVarDecl(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额外带来的是上述参数情形asParamDecl(decl)、是VarDeclBase而非VarDecl的全局声明以及与既有分支冗余的本身是全局的 struct 的字段。实际上只有少量条目按该标志分支。可见性作用域类型作用域而非文件作用域public、internal、private与其他修饰符一样被检查但它们命名的作用域不是源文件。isDeclVisibleFromScopeslang-check-expr.cpp 第 1148 行判定修饰符可见于public任意作用域包括其他模块internal声明所在模块内的任意作用域private声明的类型或命名空间以及该类型的 extensions因此private是类型作用域而非文件作用域同一文件中的自由函数不能读取struct的private成员读取会被E30600拒绝在没有外围类型可供其限定作用域的位置写private全局作用域或接口需求上会以E30603提前拒绝。dyn interface限制validateDynInterfaceUsage与validateDynInterfaceUseWithInheritanceDeclslang-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基接口E33077conforming 类型不得通过extension获得 conformanceE33078不得是泛型E33082其字段既不能是 unsizedE33079、opaqueE33080也不能非可拷贝E33081——动态表示必须是一个可拷贝、定长的盒子。着色器专用检查slang-check-shader.cpp 验证入口点函数的 stage 属性、参数修饰符in、out、inout与阶段专用 intrinsic、返回类型与 stage 的兼容性、资源绑定规则。失败以引用shader(...)属性或入口点签名的诊断呈现。以下五项检查专门针对入口点验证而非通用推断遍历值得单独说明泛型结构体 capability 需求。collectGenericStructTypeUsesslang-check-shader.cpp递归遍历入口点签名类型找出每个用户自定义泛型结构体例如Fooint包括嵌套在Optional...、数组或ConstantBuffer...内部的并对其[require(...)]做目标校验。通用 capability 推断遍历SemanticsDeclReferenceVisitor只为DirectDeclRef记录类型需求泛型特化是GenericAppDeclRef会被跳过否则需求会被丢弃。该检查刻意放在这里而非推断遍历中以免每个命名此类类型的库函数被迫重新声明 capability。带MagicTypeModifier/IntrinsicTypeModifier的内建泛型类型已有更具体的诊断被过滤掉但仍被穿过递归。目标无法满足的需求——SPIR-V 入口点签名中Fooint引用[require(cpp)] struct FooT——以E36107报告在入口点上后随 see using of Foo note。未特化的泛型入口点。void mainT(...)这类真正未特化的泛型入口点会降级为IRGeneric而非IRFunc过去会在链接期崩溃。createSpecializedGlobalAndEntryPointsComponentTypeslang-check-shader.cpp现在用Linkage::isSpecialized结合特化实参字符串是否存在来判断只在真正未特化时调用diagnoseGenericEntryPoint第 3964 行对入口点名发射E38014。冲突的深度输出。片段入口点最多写一个深度系统值。由于逐参数语义检查孤立地看待每个 semantic冲突被单独检测collectDepthOutputSemanticsslang-check-shader.cppsource_commit对应第 536 行遍历每个out/inout参数与返回类型——解开ConditionalT与数组包装、递归进入 struct 字段因此out DepthOut a[1]字段上的 semantic 也能被触达——收集计数大于一即产生Diagnostics::MultipleDepthOutputSemanticsE30705把第二个贡献者命名为与第一个冲突。系统值 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::UnhandledModOnEntryPointParameterE38010消息点名修饰符与参数并说明它将被忽略。只有[[vk::binding(...)]]情形被门控——_allTargetsSupportVkBindingOnEntryPointParameters第 1580 行遍历 linkage 的全部目标、isVkBindingCompatibleEntryPointParameterType第 920 行检查参数自身类型——使它只在该属性真会被丢弃时触发。[[vk::push_constant]]、register()、packoffset()出现在入口点参数上时无条件诊断。失败模式错误恢复与高价值诊断所有语义检查错误都流经线程进SemanticsContext的DiagnosticSink。检查级恢复的一般策略是用占位类型继续避免一个错误级联爆炸未解析的声明变成ErrorType类型重载解析返回合成的errorExpr而非中止。诊断尽力点名违规源码构造当ExpectATypeReprslang-check-type.cpp发现不表示类型的表达式时用表达式的实际类型以及可用时的被引用名构建Diagnostics::ExpectedAType消息。以下几项诊断超越了点名构造直接指向修复方向逐候选实参不匹配。调用匹配不到任何重载时诊断现在列出每个候选签名及拒绝它的具体实参。slang-check-overload.cpp在候选上记录违规实参索引与期望/实际类型再对每个候选发射Diagnostics::OverloadCandidateArgumentTypeMismatchnote。以两个float调用void g(A, int)/void g(B, float)调用处报E39999每个候选一条E40011candidate: signaturenote各随一条E40018noteargument 0 does not match: expected A, got float。候选按渲染签名字符串去重而非按Decl*否则会把foofloat与fooint这类不同特化错误合并最多打印十个唯一候选其余由E40015N more overload candidates note 汇总。未定义标识符的Did you mean ...?。名称解析失败时slang-check-expr.cpp遍历作用域内候选通过StringUtil::calcLevenshteinDistanceCaseInsensitive把保守的相似名建议附着到现有Diagnostics::UndefinedIdentifierE30015上而非发射游离 note。findClosestInScopeName第 5327 行把预算定得很具体长度小于 3 或大于 256 的名字不给出任何建议允许距离为min(3, max(1, length / 3))——大约每三个字符一次编辑下限 1 上限 3核心模块声明与作用域无法访问的内容被跳过两个不同名字打成平手时抑制建议使输出不依赖作用域遍历顺序。于是myLongVariableNam建议myLongVariableName而ac不会建议作用域内的absqr不会建议核心模块的sqrt。被丢弃的[NoDiscard]结果。maybeDiagnoseDiscardedNoDiscardResultslang-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),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门