ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Slang 声明可见性规则深度解析:public / internal / private 的语义、语言版本默认值与编译管线中的过滤位置

2026/9/18 2:05:12 拓冰建站 浏览量
Slang 声明可见性规则深度解析:public / internal / private 的语义、语言版本默认值与编译管线中的过滤位置 Slang 声明可见性规则深度解析public / internal / private 的语义、语言版本默认值与编译管线中的过滤位置【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang本文是 Slang 编译器source/slang/声明可见性机制的权威技术指南面向需要新增或修改可见性相关诊断的开发者和研究跨模块访问语义的语言设计者。文章完整讲解public/internal/private三种关键字各自携带的可见性级别、不同语言版本legacy 2018、2025、2026下的默认规则以及可见性过滤在名称查找与重载解析管线中的两个关键执行点读完后你将能够准确判断为什么另一个模块中的某个声明可访问或不可访问并理解use-of-less-visible-type30604、decl-cannot-have-higher-visibility30601、decl-is-not-visible30600等诊断的触发条件与源码依据。说明本文所依据的文档 docs/generated/design/name-resolution/visibility.md 是仓库自动生成设计文档体系中的一员其准确性经过了独立的评审review 报告与修复remediation 报告流程核验文中所有行号均以记录在案的目标提交为准。可见性的基础概念从修饰符类到内部枚举VisibilityModifier 与三个空标记子类可见性关键字在 AST 层由抽象基类VisibilityModifier及其三个具体子类表示全部声明在 slang-ast-modifier.h 第 49-67 行PublicModifier第 55 行对应public关键字PrivateModifier第 61 行对应private关键字InternalModifier第 67 行对应internal关键字。三者都是空标记类empty marker class本身不携带任何数据作用仅是在 AST 上标记声明携带了哪个可见性关键字。DeclVisibility编译器内部使用的枚举编译器其余部分推理可见性时使用的是DeclVisibility枚举定义在 slang-ast-support-types.h 第 1992-1998 行注意该文件是SemanticsVisitor的成员枚举enum class DeclVisibility { Private, Internal, Public, Default Internal, };关键事实枚举的数值顺序是Private Internal Public整个编译器中大量使用Math::Min在可见性值上求最小值以计算复合体例如参数化类型的有效可见性Default Internal是一个别名表示未显式指定时的兜底该枚举中不存在Default作为一个独立级别——任何在枚举查找后得到Default值的情况都被视为不可见。ModuleDecl 上的两个关键字段slang-ast-decl.h 中ModuleDecl有两个与可见性直接相关的字段defaultVisibility第 835 行记录模块中未携带显式修饰符的成员所采用的默认可见性。它在声明时初始化为DeclVisibility::Internal并在语义检查阶段按下文所述的规则被覆盖languageVersion第 833 行模块的语言版本字段。module声明本身并不携带版本号该字段的初始化来自编译请求compile request的LanguageVersion编译器选项slang-compile-request.cpp 第 324-339 行将选项集中的版本读入并赋给translationUnitSyntax-languageVersion随后解析器在文件使用了需要现代构造的语法时会把 legacy 值升级为 2025slang-parser.cpp 第 1221-1227 行的maybeUpgradeLanguageVersionFromLegacy在第 1260、1365、1404 行被调用。这两步位于本文档监控路径watched paths之外。语言版本常量定义在 include/slang.hSLANG_LANGUAGE_VERSION_LEGACY为2018目标提交下第 5773 行同一定义了SLANG_LANGUAGE_VERSION_2025、SLANG_LANGUAGE_VERSION_2026、SLANG_LANGUAGE_VERSION_LATEST2026 的别名以及SLANG_LANGUAGE_VERSION_DEFAULT其值就是SLANG_LANGUAGE_VERSION_LEGACY即2018——因此文档化的语言默认版本至今仍是 2018拼写错误的SLANG_LANGAUGE_VERSION_DEFAULT被保留在正确拼写旁边纯粹是为了源码兼容公开 API 的既定规则已发布的名字永不删除SlangGlobalSessionDesc::minLanguageVersion默认值为SLANG_LANGUAGE_VERSION_2025。但注意下面的可见性默认值只依赖模块解析后的languageVersion会话级的minLanguageVersion只是记录一个首选下限并不被可见性分类路径查询。IgnoreForLookupModifier容易被误认为可见性关键字的存在slang-ast-modifier.h 第 248 行的IgnoreForLookupModifier是一个独立修饰符它把声明完全从查找lookup中隐藏不属于Public/Internal/Private分类体系但经常被误认。详见下文与IgnoreForLookupModifier的交互。三种关键字各自的确切语义三个关键字映射到DeclVisibility的三个级别关键字可见范围说明public任何导入了声明所在模块的模块跨模块可达internal仅在声明所在模块内部包括该模块的所有文件模块内全部文件共享private仅在声明所在的聚合类型内部struct、class、interface等命名空间不是private的合法容器关于private有一条容易踩坑的规则命名空间成员仍然是全局声明isGlobalDecl见 slang-check-decl.cpp 第 1571-1575 行因此在命名空间成员上写private会在声明处被invalid-use-of-private-visibility诊断拒绝详见边界情况与失败模式。getDeclVisibility分类映射与三个结构性兜底关键字到枚举的映射由getDeclVisibility实现slang-check-decl.cpp 第 22214-22278 行本文已验证当前工作树实现。函数遍历decl-modifiers返回找到的第一个VisibilityModifier对应的级别。除显式关键字外它还实现了三个结构性兜底fall-throughAccessorDecl与EnumCaseDecl可见性从外层声明继承——父级遍历会跳过它们访问器/枚举成员自身不独立声明可见性GenericDecl可见性取自其inner声明即泛型声明与其参数化的声明共享同一可见性泛型参数与GenericTypeConstraintDecl当父级是GenericDecl时可见性取该泛型的inner声明的可见性父级为其他类型的GenericTypeConstraintDecl则回退到DefaultisGenericParam检查失败时直接返回DeclVisibility::Default。源码印证slang-check-decl.cpp函数先判断isGenericParam(decl) || asGenericTypeConstraintDecl(decl)若asGenericDecl(decl-parentDecl)失败则立即返回DeclVisibility::Default这正是约束声明必须由泛型声明持有才继承可见性这一限定条件的实现来源。无显式修饰符时的回退顺序若声明没有显式修饰符回退取决于声明所处位置与模块语言版本按以下顺序判定Slang 2026 及以后聚合类型的未修饰成员继承聚合自身的有效可见性。例如internal struct S { void f(); }f是internal而非采用模块默认值。该规则基于聚合的有效可见性递归调用getDeclVisibility(parentAggTypeDecl)的结果因此可以通过嵌套聚合传递组合且仅在模块languageVersion SLANG_LANGUAGE_VERSION_2026时生效保证既有 2025 与 legacy 代码保持旧行为。接口成员继承接口的可见性——这正是 2026 聚合规则刻意镜像的对象且该规则在所有语言版本下都生效。否则采用模块默认值见下节。按语言版本的默认可见性公式与模块级翻转ModuleDecl::defaultVisibility控制模块中没有显式修饰符的成员所采用的隐式可见性在getDeclVisibility中计算slang-check-decl.cppdefaultVis parentModule-languageVersion SLANG_LANGUAGE_VERSION_LEGACY ? DeclVisibility::Public : parentModule-defaultVisibility;legacy2018语言把每个未标注声明都视为public——该语言先于可见性系统存在既有代码依赖这一行为现代版本默认internal除非模块顶部声明了publiccheckModule在ModuleDecl上发现PublicModifier时会把模块级默认翻转为publicslang-check-decl.cpp 第 5147-5150 行NamespaceDecl无条件为Publicslang-check-decl.cpp。函数局部变量也吃模块默认值回退链中没有任何环节要求声明必须是某个容器的成员因此函数局部VarDecl与任何未修饰声明一样采用模块默认值且checkVisibility会对它连同所有VarDeclBase一起执行slang-check-decl.cpp 第 2957 行局部变量经由SemanticsStmtVisitor::visitDeclStmt中的ensureDeclBase(..., DefinitionChecked)到达该路径。在public module M;中这会让局部变量成为public于是容器级上限会以use-of-less-visible-type30604拒绝一个类型为internal的局部变量——尽管该名字永远不会逃出函数public module M; internal struct Counter { internal int n; } void f() { Counter c; } // error 30604示例中Counter的字段n刻意标注为internal若保持裸写同样的 public 模块默认值会让n也变成public从而在一个internal结构体内放入public成员在字段行额外触发一条不相关的错误 E30601decl-cannot-have-higher-visibilityn cannot have a higher visibility than Counter.。标注internal能让片段精确演示它想说明的函数局部变量规则。可见性在哪里被过滤查找边界与重载解析可见性在两个不同的执行点被查询二者共享同一个谓词isDeclVisibleFromScope。整体流程如下图所示1. 查找边界检查器中大多数查找调用点会把结果交给filterLookupResultByVisibilityAndDiagnoseslang-check-expr.cpp 第 1291-1301 行后者把过滤本身委托给filterLookupResultByVisibility第 1278-1289 行。如果查找返回了候选但全部被过滤掉该函数会发出decl-is-not-visible诊断Diagnostics::DeclIsNotVisibleslang-diagnostics.lua 30600并报告第一个被移除的声明。在语言服务器language-server模式下它刻意返回未过滤的结果让补全功能可以继续工作。2. 重载解析TryCheckOverloadCandidateVisibilityslang-check-overload.cpp 第 265-287 行由重载过滤管线在每一个通过参数个数/类型检查的幸存候选上调用JustTrying模式下静默丢弃不可见候选ForReal模式下发出与上面相同的DeclIsNotVisible诊断。isDeclVisibleFromScope唯一的判定谓词两个调用点都委托给SemanticsVisitor::isDeclVisibleFromScope(declRef, scope)slang-check-expr.cpp 第 1148-1264 行。谓词计算声明的DeclVisibility后分派Public— 总是可见Internal— 当且仅当getModuleDecl(decl)等于getModuleDecl(scope)即请求作用域属于同一模块Private— 当且仅当请求作用域的某个父级就是拥有该声明的外层聚合或命名空间或该作用域位于某个目标类型匹配的ExtensionDecl内部其他任何值枚举查找后不可能出现的Default— 不可见。private访问检查会沿请求作用域的父链向上查找外层聚合第 1164 行。当候选声明位于ExtensionDecl中时谓词还会解析扩展的目标类型并与请求作用域的外层聚合做类型相等性比较第 1191-1261 行借助局部类ContainerTargetTypeResolver。这使extension S { private foo() {...} }可以从S内部或S的另一个扩展中调用——即使另一个扩展是特化到S的泛型扩展。该特化步骤以候选自身的容器是否为ExtensionDecl为守卫第 1259 行因此不会反向运行。泛型类型GT自身主体内声明的private成员会被当作该类型自己的默认 decl-refGT与请求扩展的目标类型比较而该比较失败这样的成员从extension Gint和extensionT GT都不可达。只有声明在extension里的private才能跨实例化访问。容器级可见性上限getTypeVisibility复合类型取最小值SemanticsVisitor::getTypeVisibilityslang-check-expr.cpp 第 1139-1146 行计算Type的可见性取底层声明的可见性与其声明引用declaration-reference泛型参数可见性的最小值——递归只下探到本身是DeclRefType的参数。这正是HashMapString, InternalKey在HashMap为public时仍被视作internal的原因。递归本体在静态辅助函数_getTypeVisibility第 1112-1128 行中它通过一个DictionaryType*, DeclVisibility备忘录贯穿整趟遍历getTypeVisibility是每次查询创建该字典的薄入口且只缓存已完成的结果。原因在于泛型参数频繁重复在PairT, T中两个参数指向同一份 canonical 类型 DAG第二条边应复用第一条边已收集的可见性而不是重新遍历整个 DAG。checkVisibility反向约束SemanticsVisitor::checkVisibilityslang-check-modifier.cpp 第 2344-2390 行强制反向约束声明不能引用比自己更不可见的类型违反产生use-of-less-visible-type诊断Diagnostics::UseOfLessVisibleType代码 30604。同一函数还强制声明的可见性不能超过最近的外层AggTypeDeclBase的可见性。搜索从声明自身开始因此聚合声明是与自己比较该上限实际作用于非聚合成员。违反产生decl-cannot-have-higher-visibility代码 30601。相应测试可见 visibility 测试目录例如public-struct-inside-internal-struct-accepted.slang嵌套public聚合因自比较而被接受public-inside-internal-struct-rejected.slanginternal聚合内的public成员被 30601 拒绝public-field-of-less-visible-type-rejected.slang、public-function-with-internal-return-rejected.slang 与 synthesized-init-references-less-visible-type-rejected.slang30604 的三个触发面。与 extern、export 的交互extern与export修饰符与public/internal/private分类体系相互独立它们不改变声明的DeclVisibility而是直接作用于查找或跨模块可达性ExternModifier 与 ExtensionExternVarModifierExternModifierslang-ast-modifier.h 第 100 行与ExtensionExternVarModifier第 231 行标记extern声明DeclPassesLookupMaskslang-lookup.cpp 第 41-54 行总是把ExtensionExternVarModifier声明从查找中丢弃并且丢弃父级是ExtensionDecl的ExternModifier成员——因此这些extern成员无论可见性关键字如何都被隐藏在查找之外。用户层面的写法是extension的extern成员最终失败形态是缺失而非拒绝extension S { extern int extra; } // error 31143 here int test(S s) { return s.extra; } // error 30027 here声明本身报missing-original-defintion-of-extern-decl31143使用处报no-member-of-name-in-type30027member not found——而不是可见性拒绝本应产生的decl-is-not-visible30600。对应测试extern-member-of-extension-hidden-from-lookup.slang。HLSLExportModifier 与 ExportedModifierHLSLExportModifier第 112 行是裸export关键字修饰符标记声明的链接意图它本身不会提升声明的DeclVisibility。测试 export-keyword-does-not-raise-visibility.slang 验证export标记的结构体对可见性检查器而言仍只是internalExportedModifier第 142 行用在import上控制传递性跨模块可达性isModuleReachableViaExportedImportsslang-check-decl.cpp 第 9138-9161 行只跟随带有ExportedModifier的 import因此__exported import会再导出被导入模块而普通import不会。import 非传递性还必须在作用域链上强制普通import的非传递性不仅要在 import 图上强制还要在作用域链上强制。模块被导入时importModuleIntoScope会把被导入模块的作用域拼接到导入方作用域的兄弟链上——而被传递导入的外来模块的FileDecl也可能落在这条链上。谓词isOwnModuleOrIncludedFileScopeslang-check-expr.cpp 第 334-349 行决定哪些兄弟属于模块自身的再导出表面模块自身的作用域或parentDecl就是该模块的FileDecl即被__include的文件。这里的parentDecl moduleDecl合取条件是承重的外来模块的FileDecl指向的是那个模块所以无论它如何到达链上都会被丢弃——去掉这个合取条件就会再导出外来文件静默地把普通import变成传递性的见仓库 issue shader-slang/slang#11443。它同样丢弃using拼接出的命名空间兄弟因此主文件里的using namespace Foo;不会透过import泄漏。该谓词声明在 slang-check-impl.h 第 76 行由SemanticsVisitor::importModuleIntoScopeslang-check-decl.cpp 第 17077 行函数始于 17032 行以及在入口点检查期间slang-check-shader.cpp 第 3775 行应用。泛型参数、访问器与合成成员getDeclVisibility把泛型参数的可见性折叠进其泛型inner声明的可见性因此泛型参数永远不会独立地比它参数化的声明更可见或更不可见。类似地AccessorDecl与EnumCaseDecl从父级取得可见性。合成成员接口需求见证者、导数实现等大多在合成现场被显式赋予可见性而非依赖模块默认值当该赋值是条件性的时合成声明回退到上述常规有效/默认分类。需求满足合成器在满足的需求携带显式可见性修饰符时调用addVisibilityModifier(synthesized, Math::Min(parentVisibility, requirementVisibility))slang-check-decl.cpp 第 7227-7229 行用于合成方法、8305-8311 行用于属性、8684-8690 行用于下标因此合成成员永远不会比其父级或其所满足的需求更可见。合成微分类型与字段通过addVisibilityModifier(decl, getDeclVisibility(parent))直接传播父成员的可见性slang-check-decl.cpp 第 3869-3871 行与 3925-3926 行slang-check-expr.cpp 第 845、878 行合成的微分结构体及其类型别名也是如此。addVisibilityModifier本身是 slang-check-decl.cpp 第 3687 行的SemanticsVisitor方法。相关测试见 visibility 测试 READMEgeneric-function-visibility-from-inner.slang泛型取 inner 可见性synthesized-witness-capped-at-requirement-and-parent.slang见证者被需求与父级双上限约束synthesized-differential-propagates-public-visibility.slang 与 public-decl-referencing-internal-differential-rejected.slang微分类型传播可见性的正反两面accessor-inherits-property-visibility.slang访问器继承宿主可见性。合成扩展需要两种可见性而不是一种当合成产出的是extension而非普通成员时需要做两个可见性决策因为扩展被提升到模块作用域在那里private根本不是合法可见性。考虑一个标注了[Differentiable]的 private 成员函数其导数被合成为函数即类型上的扩展但该扩展自身不能是private。getSynthesizedExtensionVisibilityslang-check-decl.cpp 第 9111-9131 行通过返回一个SynthesizedExtensionVisibility对第 9073 行来解决该对包含独立的extensionVisibility与memberVisibility字段Public与Internal目标把两个字段都映射为自身Private目标把扩展映射为Internal模块可见这样合成可以附着其上同时让合成成员保持Private从而调用者的 API 表面不变其他任何值都是SLANG_UNEXPECTED因为经过分类后Default不应到达这里。相关辅助函数getMoreRestrictiveVisibility第 9079-9105 行在归一化Default之后计算两个可见性的逐对最小值是不比这两者中的任何一个更可见的共享写法。配套测试 private-differentiable-member-synthesizes-internal-extension.slang 验证了这一拆分。与 IgnoreForLookupModifier 的交互携带IgnoreForLookupModifierslang-ast-modifier.h 第 248 行的声明在可见性过滤看到它之前就被查找跳过。当前该修饰符唯一的产出者是枚举上的 tag-type 继承声明slang-check-decl.cpp 第 12295 行它被排除在查找之外使枚举的 tag type 不会在成员查找时作为基接口出现slang-lookup.cpp 第 462 行。因此可见性规则永远不会作用于这类声明——无论它们携带什么可见性修饰符。边界情况与失败模式汇总以下按诊断码组织全部边界行为附源码位置场景诊断代码源码位置public位于internal结构体内部decl-cannot-have-higher-visibility30601slang-check-modifier.cpp 第 2404-2408 行private用在不是类型成员的声明上invalid-use-of-private-visibility30603slang-check-modifier.cpp 第 2207-2216 行在不接受可见性修饰符的节点上使用invalid-visibility-modifier-on-type-of-decl36005slang-diagnostics.lua更可见的签名引用更不可见的类型use-of-less-visible-type30604slang-check-modifier.cpp 第 2385 行查找有结果但全部被过滤decl-is-not-visible30600slang-check-expr.cpp 第 1300 行slang-check-overload.cpp 第 279 行重复修饰符如public publicduplicate-modifier31202slang-check-modifier.cpp 第 2546 行冲突组检查using非命名空间目标ExpectedANamespace30061slang-check-decl.cpp 第 17349-17400 行extension的extern成员声明/使用处missing-original-defintion-of-extern-decl/no-member-of-name-in-type31143 / 30027slang-lookup.cpp 第 41-54 行public别名经普通import暴露非导出声明public-custom-derivative-uses-non-exported-import31162slang-check-decl.cpp 第 9202-9251 行要点详解private的三种非法形态不只是顶层一种模块/文件作用域声明、namespace成员两者都是isGlobalDecl、以及接口需求。报错信息说is not a member of a type这正是规则的准确表述——只有容器不够必须是类型private namespace X不会触发 36005isGlobalDecl测试先行并返回所以命名空间——无论顶层还是嵌套——都报 30603。只有当用户把命名空间标记为internal等在isGlobalDecl测试通过之后仍进入修饰符检查的路径才可能命中 36005跨语言版本导入legacy 模块导入现代模块时现代模块的声明仍按自己的defaultVisibility分类legacy 调用方的作用域经getModuleDecl(scope)检查——位于不同模块使internal声明不可见与调用方语言版本无关。而 legacy 模块自身的声明因 legacy 默认值为public对任何调用方都可见泛型类型上的extensionextension中声明的private成员可以从同一类型不同泛型实例化的扩展访问因为isDeclVisibleFromScope用applyExtensionToTypeslang-check-expr.cpp 第 1256-1263 行把候选容器类型与请求作用域容器类型对齐该对齐以候选位于ExtensionDecl中为条件所以泛型类型自身主体内的private成员得不到它会从该类型上的任何扩展处以DeclIsNotVisible被拒绝合成导数成员auto-diff 把导数合成为函数即类型其属主本身是结构体成员上的extension时isDeclVisibleFromScope会递归解析父聚合使导数继承普通成员的可见性作用域第 1208-1213 行using只能引入命名空间式容器visitUsingDeclslang-check-decl.cpp 第 17349-17400 行要求其参数解析为NamespaceDeclBase模块与命名空间同属此类——modules are namespace-like否则发出ExpectedANamespace30061。因此using SomeStruct;会以 expected a namespace 被拒绝而不是给结构体起别名using也不会再导出单个internal成员所以在监控路径中不存在using特有的可见性拒绝。非手工书写的别名与 31162FuncAliasDecl从不手工书写它由 AD-2.0 对[ForwardDerivative(...)]/[BackwardDerivative(...)]属性的翻译合成后者在原始函数即类型上构建扩展并给出一个命名所提供导数的别名成员slang-check-decl.cpp 第 19342 行的translateFwdDerivativeAttributeToAD2第 19298 行反向导数的孪生实现始于第 19210 行。被拒绝的形态因此是public原始函数其导数来自仅被普通导入的模块import helper; // not __exported import [ForwardDerivative(helper_fwd)] // error 31162 here public float f(float x) { return x * x; }该拒绝路径是validatePublicCallableOperandVisibility的FuncAliasDecl分支slang-check-decl.cpp 第 9202-9251 行别名分支在 9211-9223 行public别名目标不是Public或目标所在模块只经由普通非__exportedimport可达时报public-custom-derivative-uses-non-exported-import31162。测试覆盖与验证策略可见性规则由 docs/generated/tests/design/name-resolution/visibility 目录下的测试包系统性验证其 README 说明了完整策略正/负配对每条强制规则一个正向测试证明从关键字允许的作用域可以访问 一个负向测试证明从禁止的作用域被拒绝。正向用例在//TEST:INTERPRET(filecheckCHECK):下运行需要-std选择时改用//TEST:SIMPLE ... -target hlsl负向用例使用//DIAGNOSTIC_TEST:SIMPLE(diagCHECK):并带 caret 列标注。可见性在语义检查阶段即已解析、先于任何目标降级因此没有逐后端发射的扇出——唯一的例外是 auto-diff 合成声明需要运行时观察使用COMPARE_COMPUTE -cpu版本配对测试Slang 2026 聚合继承规则在-std 2025与-std 2026下用同一份源码各测一次让版本门槛本身成为观察对象见 aggregate-member-takes-module-default-2025-accepted.slang、aggregate-member-takes-module-default-2025-rejected.slang、aggregate-member-inherits-internal-visibility-2026.slang、aggregate-member-inherits-public-visibility-2026-rejected.slang 等合成扩展的拆分可见性private[Differentiable]成员的导数扩展必须为internal而非private也有专门测试单文件局限跨模块声明internal在另一模块不可见、__exported import再导出、isOwnModuleOrIncludedFileScope非传递性无法在一个.slang源中表达记录在 README 的 Untested claims 表中其中internal跨模块不可见的手写覆盖存在于tests/diagnostics/extension-visibility-*.slang测试包中的_repro/目录存放非测试复现脚本无//TEST指令运行器会跳过。关联阅读scopes.md —isDeclVisibleFromScope所遍历的作用域链lookup.md — 其查找结果被可见性过滤的查找算法overload-resolution.md — 调用TryCheckOverloadCandidateVisibility的重载候选过滤03-semantic-check.md — 运行可见性过滤的语义检查阶段modifiers.md — 每个修饰符的逐类参考含可见性家族declarations.md —ModuleDecl、NamespaceDecl与聚合类型声明的逐类参考glossary.md —visibility modifier、decl-ref、name resolution词条。【免费下载链接】slangMaking it easier to work with shaders项目地址: https://gitcode.com/GitHub_Trending/sl/slang创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考