从快照测试看 Roc 平台的 exposes 机制:文档生成如何隐藏内部模块
从快照测试看 Roc 平台的 exposes 机制文档生成如何隐藏内部模块【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc本篇技术指南以 Roc 编译器仓库中的文档生成快照测试 test/snapshots/docs_platform_hides_internal_modules.md 为核心深入剖析 Roc 平台platform头部exposes声明如何划定公开 API 边界并说明文档生成管线如何据此只收录被暴露的模块、隐藏内部宿主模块。读完本文你将掌握 Roc 平台头部各字段requires/exposes/packages/provides/hosted/targets的语义与写法、package-docs文档输出格式的结构以及从快照源码到编译器实现src/docs/extract.zig、src/compile/module_discovery.zig的完整证据链并能在本地用快照工具复现验证。这份快照在验证什么Roc 仓库的 test/snapshots/README.md 说明快照测试通过捕获源码在编译管线各阶段tokenization、解析、canonicalization、类型检查、文档生成等的期望输出来验证编译器行为用于在行为意外变化时检测回归。本文的主角docs_platform_hides_internal_modules.md属于typedocs快照其 META 头描述为descriptionPlatform docs hide internal mods not listed in exposes typedocs它验证的核心行为是为平台生成文档时未被exposes列出的内部模块不会出现在文档中。快照由三部分组成SOURCE一段最小可编译的 Roc 平台源码main.roc、Stdout.roc、Host.roc三个文件DOCS文档生成阶段应输出的规范 S 表达式S-expression结果两者对照即可确认被暴露的Stdout模块进入了文档而内部宿主模块Host被隐藏。平台头部exposes声明公开 API 的边界快照的SOURCE中main.roc是一个结构完整的平台定义。下面先完整给出源码再逐字段拆解platform requires {} { main! : () {} } exposes [Stdout] packages {} provides { roc_main: main_for_host! } hosted { roc_stdout_line: Host.stdout_line!, } targets: { inputs_dir: targets/, x64glibc: { inputs: [app] }, } import Host import Stdout main_for_host! : () {} main_for_host! main!各字段语义如下字段本例写法作用platform 空字符串名称声明这是一个平台platform根模块名称留空表示本地开发用最小平台requires {}{ main! : () {} }声明平台要求的应用接口即应用必须提供main!能力exposes [Stdout]仅列出Stdout公开 API 清单只有这里列出的模块/名称才属于平台对外文档与可见面packages {}空集合平台依赖的外部包集合provides{ roc_main: main_for_host! }平台向宿主host提供的入口符号如roc_mainhosted{ roc_stdout_line: Host.stdout_line! }平台从宿主环境请求的效应effect能力如stdout_linetargetsinputs_dirx64glibc构建目标配置输入目录与目标后端关键点在于exposes [Stdout]它只暴露了Stdout一个模块名。Host虽然被import Host引入并且其stdout_line!能力出现在hosted块中但Host没有出现在exposes列表里——这正是它要从文档中消失的原因。从源码看平台头部的处理由 src/compile/compile_build.zig 中的setHeaderPublicSurface完成对应.platform分支见该文件 第 1315-1319 行而公共表面public surface的提取逻辑统一在 src/compile/module_discovery.zig 的extractPublicSurface中实现。该函数只依据根模块源码来分类头部公开名称并用PublicSurfaceKind枚举区分package与platform两种语义见 module_discovery.zig 第 47-51 行。公开模块与内部模块的分工Stdout与Host平台源码里有两个被main.roc导入的模块一个公开、一个内部。公开模块Stdout.roc——它被exposes列出同时携带面向文档的注释import Host ## Public standard output helpers. Stdout : [].{ ## Write a string to standard output. line! : Str {} line! |message| Host.stdout_line!(message) }这里##开头的行是文档注释doc comments模块级注释Public standard output helpers.与条目级注释Write a string to standard output.。它们会原样进入生成的文档作为package-docs中对应条目的doc字段。内部模块Host.roc——它代表宿主边界仅有内部用途## Internal host boundary. Host : [].{ ## Internal host effect. stdout_line! : Str {} }注意它的文档注释明确写着Internal host boundary.内部宿主边界与Internal host effect.内部宿主效应。尽管Host被Stdout实际调用Host.stdout_line!(message)并且作为效应能力出现在平台的hosted块中但由于它不在exposes列表里它在文档生成阶段被整体过滤掉。期望输出package-docs中只保留被暴露的模块快照的DOCS部分是文档生成阶段必须原样输出的规范格式完整如下(package-docs (name test-app) (mod (name Stdout) (package mod) (kind type_mod) (entry (name Stdout) (kind nominal) (type Stdout : (tag-union)) (doc Public standard output helpers.) (entry (name line!) (kind value) (type (fn! (type-ref (name Str)) (record))) (doc Write a string to standard output.) ) ) ) )这份输出可以拆解出几层结构package-docs根节点name test-app表示当前文档包名mod节点name Stdout是模块名kind type_mod表示这是一个类型模块type module。注意package mod这一字段根据快照 README 的说明快照后处理会全局把被移除的头部关键字重写为mod该重写同样作用于 S 表达式输出内部entry节点模块主类型Stdoutkind nominal名义类型、类型为标签联合(tag-union)doc字段收录了模块文档注释嵌套entry成员line!kind value类型为(fn! (type-ref (name Str)) (record))——即一个接收Str、返回{}记录的能力函数doc字段为Write a string to standard output.。最重要的观察整个输出中没有任何Host相关的mod或entry。这就是快照名字的含义——platform docs 隐藏了未列入exposes的内部模块。文档输出以 S 表达式序列化对应实现位于 src/docs/DocModel.zig如第 28 行附近的(package-docs写入逻辑以及type_module类型常量渲染层则由src/docs/下的render_markdown.zig/render_html.zig完成。源码级原理文档提取如何按exposes过滤快照展示的是结果其背后的过滤逻辑在 src/docs/extract.zig 中实现。核心流程分三步第一步构建暴露名单。提取开始时把传入的exposed_names即头部exposes [Stdout]解析出的名称填入哈希集合var exposed_names: std.StringHashMapUnmanaged(void) .empty; if (options.exposed_names) |names_to_expose| { try exposed_names.ensureTotalCapacity(gpa, intCast(names_to_expose.len)); for (names_to_expose) |exposed_name| exposed_names.putAssumeCapacity(exposed_name, {}); }见 extract.zig 第 335-340 行第二步按暴露名单过滤定义。遍历模块的全部定义defs时只要设置了exposed_names就跳过不属于暴露名单的条目for (defs_slice) |def_idx| { if (options.exposed_names ! null) { const entry_name defEntryName(module_env, def_idx) orelse continue; if (!isUnderExposedName(exposed_names, entry_name)) continue; } ... }见 extract.zig 第 359-363 行同时注释明确说明设计意图For documentation purposes, show all accessible definitions, not just whats explicitly exported. Exports control compilation/linking (what other modules can import), but docs should be comprehensive.——导出exports控制编译与链接可见性而这里exposes额外约束了文档的可见范围。第三步根名匹配。isUnderExposedName负责判定一个条目是否属于某个被暴露的名称。它对带点号的限定名qualified name取第一个.之前的根名进行匹配fn isUnderExposedName(exposed_names: *const std.StringHashMapUnmanaged(void), name: []const u8) bool { const root_name if (std.mem.findScalar(u8, name, .)) |dot| name[0..dot] else name; return exposed_names.contains(root_name); }见 extract.zig 第 830-833 行这意味着只要根模块名在exposes中其嵌套条目例如Stdout.line!也会一并纳入文档而根名不在名单中的模块本例的Host则连根被剔除。类似的过滤也作用于类型声明alias/nominal扫描路径见 extract.zig 第 395 行附近保证类型层面也不会泄漏内部符号。此外extract.zig 还针对不同类型模块给出默认规则普通应用/包/类型模块默认展示全部可访问定义而平台与 hosted 模块只文档化显式 provide 的条目见 extract.zig 第 342-357 行与快照所验证的平台文档隐藏内部模块语义一致。模块发现阶段public surface 如何派生在编译更早的阶段头部exposes信息就已经被结构化为公共表面。 src/compile/module_discovery.zig 定义了PublicModule、PublicSurface与PublicSurfaceKind其中extractPublicSurface的注释明确了分类规则Classify a root headers public names using only declarations in that source. Package entries name modules. A platform entry names a module only when a same-file local module import binds that name; every other entry is owned by the platform root.见 module_discovery.zig 第 53-66 行即包的exposes条目通常指向模块而平台的exposes条目中只有被同文件本地导入绑定了名字的才视为模块本例Stdout正是通过import Stdout绑定其余条目归属平台根。这条规则保证文档生成阶段能够准确地把被暴露的模块与平台根自身的名称区分开从而只把Stdout当作独立模块文档化。该工具同时服务于 IPC 路径默认roc命令与 BuildEnv 路径roc check/roc build确保两条路径行为一致。在本地复现与验证如果你想亲自验证这份快照可以按照 test/snapshots/README.md 提供的方式操作# 生成/更新全部快照 zig build run-snapshot-tool # 只更新指定快照 zig build run-snapshot-tool -- test/snapshots/docs_platform_hides_internal_modules.md # 当编译器行为变化、需要把期望输出更新为当前结果时 zig build run-snapshot-tool -- test/snapshots/docs_platform_hides_internal_modules.md --update-expected前提是本地具备 Zig 工具链并能完成构建构建细节可参考仓库根目录的 BUILDING_FROM_SOURCE.md。若某次改动导致Host意外出现在文档中快照对比会立即标红这正是typedocs快照的价值把文档只反映公开 API这一约定固化为可回归测试的期望输出。小结通过这份快照测试可以看到 Roc 平台文档生成的一条清晰链路头部exposes声明公开模块清单 →module_discovery依据头部与本地导入绑定关系派生公共表面 →extract.zig用暴露名单过滤模块定义与类型声明 →DocModel序列化出只含公开模块的package-docs输出。Host这样的内部模块即便承载核心效应、即便被公开模块调用只要不进exposes就不会出现在平台文档中——文档即 API 契约在这里由编译器强制执行而非依赖作者自觉。理解这一机制对编写自己的 Roc 平台自定义Stdout、宿主效应与构建目标以及排查文档缺失问题都极具实操价值。【免费下载链接】rocA fast, friendly, functional language.项目地址: https://gitcode.com/GitHub_Trending/ro/roc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考