Roc 包文档投影机制解析:嵌套类型如何通过公开别名生成独立文档模块
2026/9/18 21:49:25 网站建设 项目流程

Roc 包文档投影机制解析:嵌套类型如何通过公开别名生成独立文档模块

【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc

Roc 编译器的文档提取系统(docs extraction)在处理package声明时,会把导出的嵌套类型(nested source types)通过其公开别名(public aliases)投影(project)为独立的文档模块,并同步重写类型签名中的交叉引用。本文以仓库中的快照测试 docs_package_exposed_nested_type_aliases.md 为骨架,结合 src/docs/extract.zig、src/docs/DocModel.zig 与 src/snapshot_tool/main.zig 的源码实现,完整还原这一机制的工作原理、S-expression 输出格式与测试验证方式,帮助读者理解"公开别名投影"在 Roc 文档管线中的具体语义。

一、关联文档定位:一份 docs 类型的快照测试

test/snapshots/docs_package_exposed_nested_type_aliases.md是 Roc 编译器仓库中的一份docs 类型快照type=docs)。根据 test/snapshots/README.md 的说明,快照测试通过捕获编译各阶段(tokenization、parsing、canonicalization、type checking 等)的输出,来验证编译器行为并防止回归。

docs 快照使用三段式结构:

  • META:以~~~ini包裹的元信息,其中description=Package docs project nested source types through their public aliases一句话点明本测试的意图——包的文档系统把嵌套的源类型通过其公开别名投影出来type=docs表明这是一份文档提取快照。
  • SOURCE:以## <文件名>.roc分节的多文件 Roc 源码(由 src/snapshot_tool/main.zig 中的parseMultiFileSource解析)。
  • DOCS:以~~~clojure包裹的、对提取结果进行 S-expression 序列化后的期望输出。

也就是说,这份文件既是测试用例(SOURCE 是输入),又是回归基准(DOCS 是期望输出),同时还是一份"文档系统如何呈现嵌套类型别名"的权威说明。

二、测试场景还原:用as把嵌套类型导出为公开别名

SOURCE 部分包含两个文件。

包入口main.roc

package [Container.Blub as Foo, Container.Other as Bar] {}

这是一个典型的包(package)入口声明。package [...]中的每一项都是对源模块内某个顶层类型(type module)的投影声明

  • Container.Blub as Foo:把源类型Container的嵌套类型Blub导出到公共命名空间,并重命名为Foo
  • Container.Other as Bar:同理,把Other导出并重命名为Bar

注意as之后的Foo/Bar与源名称完全不同——这正是本测试要验证的核心场景:文档页面必须使用公开别名(Foo/Bar)来命名模块,而不是源类型名(Blub/Other)

类型定义文件Container.roc

## Private parent documentation. Container :: [].{ ## Public Blub documentation. Blub :: [].{ to_other : Blub -> Other to_other = |_| crash "not implemented" } ## Public Other documentation. Other :: [].{ to_blub : Other -> Blub to_blub = |_| crash "not implemented" } Private :: [].{} }

这里Container是一个不透明(opaque,::)的 tag-union 类型,内部嵌套了三个成员:

嵌套类型声明方式文档注释是否导出
Blub::(opaque)## Public Blub documentation.是(as Foo
Other::(opaque)## Public Other documentation.是(as Bar
Private::(opaque)否(未在 package 中导出)

Blub上定义了方法to_other : Blub -> OtherOther上定义了方法to_blub : Other -> Blub,两个方法在类型签名中互为交叉引用(用crash "not implemented"占位实现,仅用于类型检查)。而Private虽然嵌套在同一个Container中,但没有出现在package导出列表中,因此必须被文档系统过滤掉。

三、DOCS 期望输出逐段解读:投影后的文档 S-expression

package-docs提取结果以确定性的 S-expression 序列化((package-docs ...)),由 src/docs/DocModel.zig 中的PackageDocs.writeToSExpr生成。本测试的 DOCS 部分完整如下:

(package-docs (name "test-app") (mod (name "Bar") (package "mod") (kind type_mod) (doc "Public Other documentation.") (entry (name "Bar") (kind opaque) (type "Bar :: " (tag-union)) (doc "Public Other documentation.") (entry (name "to_blub") (kind value) (type (fn (type-ref (mod "mod.Bar") (name "Bar")) (type-ref (mod "mod.Foo") (name "Foo")))) ) ) ) (mod (name "Foo") (package "mod") (kind type_mod) (doc "Public Blub documentation.") (entry (name "Foo") (kind opaque) (type "Foo :: " (tag-union)) (doc "Public Blub documentation.") (entry (name "to_other") (kind value) (type (fn (type-ref (mod "mod.Foo") (name "Foo")) (type-ref (mod "mod.Bar") (name "Bar")))) ) ) ) )

这份输出蕴含了投影机制的五个关键语义:

  1. 别名即模块名(mod (name "Bar") ...)(mod (name "Foo") ...)两个顶层模块,模块名直接取公开别名,而非源名称Other/Blub
  2. 模块类型为type_mod(kind type_mod)表明这是"类型模块"——由某个类型投影而来。ModuleKind.type_module枚举定义于 src/docs/DocModel.zig,序列化字符串为type_moduletoStr输出"type_module"(快照后处理会把type_module重写为type_mod,见 test/snapshots/README.md)。
  3. 文档注释上浮为模块文档:源类型上的## Public Other documentation.被提升为Bar模块的(doc ...),同时保留在类型条目自身的(doc ...)中。这是因为投影后"类型页面"即"模块页面",源类型注释成为页面注释(对应 src/docs/extract.zig 中module_doc_extract的处理逻辑)。
  4. 类型以 opaque 呈现(type "Bar :: " (tag-union))::表示不透明类型,(tag-union)表示底层是 tag-union,但具体标签并未展开——即文档只暴露类型身份,不暴露内部构造器。
  5. 类型引用按别名重写to_blub的类型被序列化为(fn (type-ref (mod "mod.Bar") (name "Bar")) (type-ref (mod "mod.Foo") (name "Foo"))),即方法参数是"当前模块 Bar 中的 Bar 类型",返回值是"模块 Foo 中的 Foo 类型"。源文件里写的Other -> Blub在文档模型中被重写为公开别名路径mod.Bar/mod.Foo,保证了所有交叉引用都指向投影后的公开页面。

四、源码实现:extract.zig 中的投影管线

投影行为的核心实现在 src/docs/extract.zig。它围绕PublicTypeProjection结构组织:

pub const PublicTypeProjection = struct { public_name: []const u8, // 公开别名,如 "Foo" package_name: []const u8, // 所属包显示名,如 "mod" source_env: *const ModuleEnv, // 源模块环境 source_identity: *const [32]u8, // 源模块身份哈希 source_decl: CIR.Statement.Idx, // 源类型声明 public_order: u32, // 在包公开表面的声明顺序 };

定义于 src/docs/extract.zig,并用sortPublicTypeProjections(L248-L250)按"源身份哈希 + 公开顺序 + 公开名"排序,以便后续二分查找路由。

4.1 名称重写:projectedEntryNamerebaseEntryForProjection

投影之后,条目名必须从源名换成公开名。projectedEntryName 完成这一映射:若条目名就是投影根,则直接替换为selected.public_name;若是投影根的子成员,则保留后缀,例如Blub.to_other会被重写为Foo.to_other。而 rebaseEntryForProjection 会就地更新每个条目的名字。

4.2 归属判定:nameIsAtOrUndernameBelongsToProjection

投影必须精确划走"属于该类型"的所有定义。nameIsAtOrUnder 判断一个名称是否位于某根之下(根自身或根.子路径),nameBelongsToProjection 与 statementBelongsToProjection 据此过滤定义集。在本测试中,Private不属于任何投影根,因此不会进入任何文档模块——这正好印证 DOCS 输出里没有任何Private条目。

4.3 引用路由:projectedTypeReferenceselectPublicProjection

方法签名里的类型引用需要解析到"应该指向哪个公开模块"。projectedTypeReference(L715-L741)先根据引用是本地(local)还是外部(external)取出(identity, target_statement)对,再交给selectPublicProjection(L743-L777):

  • 若当前正处于某个投影中,且该投影包含此 identity 与声明,则直接命中当前投影;
  • 否则在排好序的public_types上按source_identity二分查找,再在所有候选投影中挑选"最近的公开根"(root_len最大者)作为命名空间所有者。

最终annotatedTypeReferenceDisplay(L787-L818)把命中投影的类型引用渲染成mod.<别名>路径 + 别名类型名。这就是 DOCS 中出现(type-ref (mod "mod.Foo") (name "Foo"))的来源——源文件里的Blub/Other在此被替换成了公开别名。

4.4 提取入口与过滤

extractModuleDocsWithOptions(L266 起)接收ExtractOptions(含exposed_namespublic_typepublic_types等,见 L217-L234),逐条过滤定义并递归提取子条目。模块名(L307-L310)与本地模块路径(L319-L321)都优先取public_name,保证文档页面永远以公开别名命名。

五、数据模型与确定性序列化:DocModel.zig

提取结果统一装入 src/docs/DocModel.zig 的数据模型:

  • PackageDocs(L10)持有包名与模块列表,writeToSExpr/writeToSExprIndented(L22-L38)输出(package-docs (name ...) (mod ...)*)
  • ModuleKind(L734-L750)区分app / module / package / platform / type_module,快照中看到的type_mod即由此枚举序列化而来;
  • DocEntry支持递归的children,使Foo -> to_other这样的嵌套条目结构得以表达(本测试中方法作为类型条目的子条目出现)。

此外还有两个重要的后处理步骤:

  1. resolveDocRefs(L42-L52):解析文档注释中的简写引用(如[Str][Utf8.default]),通过PackageDocRefResolver把它们路由到最终文档布局中的具体页面/锚点;
  2. reshapeBuiltin(L67-L130):把编译器内部的巨型Builtin类型展开为每个内建类型(StrListNum等)独立成模块,并重写所有相对引用——与"别名投影"一样属于"把内部结构重塑为面向用户的文档页面"的机制。

对快照而言,S-expression 序列化的确定性至关重要:模块按moduleDocsLessThan排序(快照工具中 main.zig#L3645),确保同一输入永远产生相同输出,快照比对才可靠。

六、快照如何运行与验证:processDocsSnapshot 全流程

docs 快照由 src/snapshot_tool/main.zig 中的processDocsSnapshot处理,流程分六步:

  1. 多文件解析parseMultiFileSource(L3758 起)把 SOURCE 段按## xxx.roc子标题拆成若干SourceFile
  2. 临时目录构建:把源码写入临时目录,用BuildEnv.build执行真实的包构建(L3555-L3574);
  3. 收集公开类型投影:遍历build_env.getCompiledPublicModules(),为每个有public_type_decl的模块构造PublicTypeProjection(L3595-L3612),并排序;
  4. 逐模块提取:对每个 documentable module 调用extractModuleDocsWithOptions,传入exposed_namespublic_typepublic_types等选项(L3615-L3641);
  5. 组装并序列化:把所有ModuleDocs装入PackageDocs(名为test-app),调用writeToSExpr输出 S-expression(L3647-L3667);
  6. 比对与写入:把新生成的 DOCS 与文件中的既有 DOCS 段比对——update模式直接覆盖,check模式不一致即失败并给出 diff,none模式仅告警(L3670-L3700)。

日常用法(见 test/snapshots/README.md#L36-L42):

# 生成所有快照 zig build run-snapshot-tool # 只更新指定快照 zig build run-snapshot-tool -- <file_path> # 从 problems 更新期望输出 zig build run-snapshot-tool -- <file_path> --update-expected

七、与兄弟快照的横向对照

同一目录下还有几个与之互补的 docs 快照,可以交叉理解"嵌套类型投影"的边界:

  • docs_package_exposed_nested_type_cross_module.md:嵌套公开类型位于不同源文件First.FooSecond.Bar,其中Bar:=声明的 nominal 记录类型),验证跨模块引用(Foo.to_bar : Foo -> Second.Bar)也能被投影路由到正确的模块页面;
  • docs_type_module.md:app场景下带文档注释的 type module(Color),展示(doc ...)从类型注释上浮为模块文档的通用行为;
  • docs_package_hides_private_modules.md:验证未导出的私有模块不会出现在 package docs 中——与本测试中Private类型被过滤属于同一设计原则。

这些测试共同构成了"公开表面(public surface)决定文档可见性"的完整保障:只有被包导出(或 app/platform 提供)的类型才会被投影为文档模块,源文件内部的私有细节一律不泄漏。

八、总结与实战要点

通过这份快照及其源码实现,可以得到关于 Roc 包文档提取系统的几个可复用结论:

  1. package [A.B as C]不只是编译期导出声明,也是文档命名空间声明:嵌套类型被投影后,其公开别名成为文档模块名,源类型名不再出现在文档页面上。
  2. 类型签名中的交叉引用会按投影路由重写Other -> Blub会变成指向mod.Bar/mod.Footype-ref,保证文档内链接始终有效(源码依据:projectedTypeReference、selectPublicProjection)。
  3. 文档注释随投影上浮:被投影类型的##注释同时成为模块文档与类型条目文档;私有父类型Container的注释("Private parent documentation.")则不会出现在任何投影模块中。
  4. opaque 类型的构造器细节不进入文档(type "Bar :: " (tag-union))只保留"不透明 tag-union"这一事实,避免暴露内部表示。
  5. 快照测试以真实构建 + 确定性序列化保证回归安全:任何改变投影行为、命名规则或 S-expression 格式的改动,都会在zig build run-snapshot-tool中被捕获。

【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询