文档管线
一份 .not 文档、一个 .notc 模块,如何变成最终呈现的内容树?本文跟着源码把这条管线走一遍。读完应该能回答四个问题:哪一段不依赖环境、名字在哪里变成函数、两种语言在哪里汇合、求值发生在哪。
一图总览
读法:圆角是数据,矩形是处理,嵌套表示包含关系。左列跟随一份 Markup 文档,右列跟随一个 Code 模块,两列在 Checker 会话汇合。
绿色是 Code 对所选前端的两次借用,每轮往返共用一个编号:① 定界并解析字面量,② 把字面量 Lower 成 IR₁。
蓝色是 Checker 会话内的往返:检查器交出字面量的 IR₁ 和当前词法绑定,收回已完成的 Item 森林。
两条入口,一个后端
Markup 经 Parse 生成前端语法树,Lower 成未解析的 Expr 森林(IR₁)与注解列表。共享后端依次完成 Resolve(名称与契约检查)、Shape(节与内容槽整理)、Materialize(挂载注解并生成 Item)。文档入口添加 Doc 根。阶段契约见 Markup 管线。
Code 将 [ ... ] 的正文交给所选 Markup 前端:ContentParser 解析字面量边界与语法,ContentLowering 将其转为 IR₁,嵌入函数体:
函数体 Expr(Code IR,还不认识任何名字)
└─ Content
├─ forest: Vec<Expr> ← 完整的 Markup IR₁
├─ annotations: Vec<Annotation>
└─ body / span
两种语言的 Parse / Lower 都处理语法结构,名称绑定与类型检查在装配环境后进行。
Checker 会话:名字在这里变成实例
分析从一份装配好的 Context 开始(签名注册表、未检查函数体、模块作用域、transform 计划,只读), Checker 会话在其上按需工作。两个方向都会用到它:
文档 Resolve 遇到 Code 调用(如
#diagrams.diagram(...))时,向会话请求具体实例,并按其签名检查调用。会话根据编译期实参、参数类型与局部绑定,将函数体 Expr 检查为 TypedExpr。遇到
Expr::Content时,将 IR₁ 与词法绑定交给共享后端,返回的 Item 森林嵌入已检查表达式:
已检查的函数体:TypedExpr
├─ ty: Option<Type>
└─ kind: ExprKind::Content(Vec<Item>)
Item 既是共享内容后端的输出,也可嵌入已检查的 Code 表达式。实例结果保存在 CheckedProgram;泛型分支按编译期条件选择,只检查选中分支。
求值
Analysis 保存求值前 Item 树与实例缓存,Evaluate 在其上工作:按 Item.pending 执行内容实现,replace 扩展在此挑选同签名目标,结果森林替换调用边界内的 children。它保留原语义树、不重新 Shape;失败在内容边界恢复,其余内容继续。契约见 Code 类型与函数系统 与 函数映射。CLI 的 core / json / query 默认看求值前,--eval 看求值后。
对象速查
对象 | 职责 | 生命周期 |
|---|---|---|
CodeModule | 单个 .notc 的 Parse / Lower 结果:签名、函数体、静态 imports | Context 装配前产生 |
函数体 Expr | 未检查表达式树,字面量位置为 | 与实例无关,可复用 |
Markup IR₁ | 未解析 Expr 森林与注解 side list | Lower 后固定,各实例共享 |
Environment | 宿主装配结果:模块源码、配置位置、Context | 宿主持有,不可变 |
Context | Registry、CodeProgram、模块作用域、ExtensionPlan | 装配完成后只读 |
Checker | 当前模块、编译期实参、局部绑定、诊断、CheckedProgram | 每次会话创建 |
TypedExpr | 实例的已检查表达式树,内容分支是 Item 森林 | 存于 InstanceRecord.body |
CheckedProgram | ResolvedInstance → 签名、TypedExpr、诊断、状态;伴随源码与定义名称快照 | 随 Analysis 保留;独立检查只导出报告 |
Context 保存模块作用域;Checker.scope 保存局部绑定与当前实例的编译期实参。
宿主入口只有几个:analyze / inspect 跑完整文档(inspect 额外保留解析树与中间 IR),compile_module 只出 CodeModule,analyze_content_literal 一次编译单个字面量,evaluate 求值;Vault.analyze_code 是宿主侧的 Code 文件检查入口。
宿主装配:Context 从哪来
装配以当前文件为入口。Markup 文档先经前端拿到 import 列表,宿主沿相对路径递归加载 .notc(读取、Parse / Lower、安装、绑定作用域),得到这份文档的 Environment。配置里每条输出规则带自己的 imports 表,独立加载、独立校验签名,不碰文档和其他规则的绑定;规则语义见 本地模块。纯 Pipeline 不碰文件:读取与缓存都在宿主,内存输入与 Worker 输入走同一加载路径。
Vault 按规范路径与文档源码快照缓存不可变的 Environment:同一文件无论经相对、绝对还是含 . / .. 的写法都共享身份;输入变化时创建新的资源视图与 Vault,缓存不做变更追踪。
LSP 与 CLI 的 Code 检查共用 Vault.analyze_code:用编辑中的源码覆盖资源视图,Environment.load_from 只加载该模块自身的 import 图,再以 check_module 检查——以编译期参数均有默认值的函数为种子,其余实例在调用处按需触发。CodeAnalysis 只导出声明、诊断与来源源码,不读输出配置、不创建 Doc 根、不求值。
宿主负责资源取得、环境装配与文档间关系,以下边界由 Vault 统一维护。
Vault 内容边界与索引
三个目录范围
范围 | 确定方式 | 作用 |
|---|---|---|
Vault 内容根 |
| 确定可纳入正文与本地引用的内容边界 |
文档输出配置 | 最近的 Notist.toml,或显式 --config | 选择输出规则及其局部 imports;文档名称绑定由源码 import 决定 |
索引扫描目录 |
| 确定首先扫描哪些文档;随后补载实际链接到的 Vault 内正文 |
扫描子目录不缩小 Vault 内容边界。Code 模块和组件实现资源可以位于 Vault 外;Resources 的根表示内容边界,资源访问范围由宿主实现决定。导入模块不会自动把其文档加入正文索引。
引用检查
本地 link 与 embed 按文档目录解析并规范化。最终目标越过 Vault 根时,在引用节点产生 Semantic 诊断,并停止补载该目标;../ 在最终目标仍位于 Vault 内时合法。带 scheme 或 // 的外部 URL 保持外部链接,不作为 Vault 正文读取或发布。
边界诊断附加到 Vault.analyze / inspect 的 Analysis,供查询与语言服务等宿主共用。纯 Pipeline 接收源码与 Context;内容边界检查由持有 Vault 根的宿主执行。VaultIndex.from_documents 对显式传入的根执行相同检查,已有诊断不重复添加。
Vault.index 从各文档处理结果派生 VaultIndex,保存索引、诊断、链接与 backlinks。它扫描指定目录,再补载 Vault 内实际链接到的正文;检查结果和 backlinks 的路径均相对于 Vault 根。
文档扩展边界
静态 import 装配后,beforecheck 接收可变 IR₁、节点注解与文档属性;修改结果完整进入 Resolve、Shape 与 Materialize。aftercheck 校验只读语义 Item,after_evaluate 编辑具体输出 Item。ExtensionPlan 调度有序 hooks 与同签名执行映射;内容字面量与 Code 模块不触发文档 hooks。契约与失败处理见 文档扩展。