文档前端
文档前端负责源码解析与 AST lowering,输出共享的未解析 Expr 森林(IR₁)。Markup 管线 注册和选择前端,再执行 Resolve → Shape → Materialize;notist-core 定义共同接口及输出类型。
接口与输出
extensions 声明可处理的文件扩展名,不包含点号。compile 接收一份源码,组合本前端的 parse → lower;前端可以持有配置和状态,并满足 Send + Sync,供语言服务等宿主使用。
cache_key 为 notc 模块编译提供可选的缓存身份。FrontendCacheKey.new::<Self>(version, configuration) 包含实现类型、语义版本和完整配置;相同身份及输入须产生相同结果,包括 ContentParser 与 ContentLowering 的行为。内置前端提供身份,自定义前端默认返回 None。
FrontendOutput 将模块声明与内容表达式分别输出。字段为:
字段 | 含义 |
|---|---|
imports | Vec<Import>;文件级静态依赖,保存相对路径、ImportKind 与源码范围 |
forest | Vec<Expr>;名称尚未解析的表达式森林 IR₁ |
annotations | Vec<Annotation>;与 forest 并列的节点注解,保留到 Shape 之后挂载 |
module_attrs | LiteralDict;由模块注解得到的文档根属性,保留字面量 Expr |
diagnostics | Vec<Diagnostic>;源码解析及 lowering 产生的诊断 |
syntax | Option<Box<dyn FrontendSyntax>>;按需保留的前端解析产物 |
前端不读取文档文件,不装配模块或 Registry。构造器名称解析、签名与 children 校验在共享后端进行;语法错误由前端汇入 Syntax 阶段,字面量及注解 payload 错误由 lowering 汇入 Semantic 阶段,注解目标诊断由 Materialize 产生。每份结果都保留原始源码的 UTF-8 字节范围。
公共 IR
Expr 区分已知数据字面量 Literal、待解析路径字面量 PathLiteral、名称引用 Ref、成员访问 Member 与调用 Call。PathLiteral 保存路径拼写和范围,Resolve 根据源文件与逻辑根构造 Value::Path,不读取资源。Call 的 callee 是表达式;简单名称使用 Ref,限定调用使用嵌套 Member。引用本身不调用函数,也不建立内容节点,源码中的 # 不作为 IR 运算保留。
块内容序列以 Fragment 保存硬分隔之间的语法分组,单换行不结束片段。前端不把片段伪装为 paragraph 调用;Resolve 根据具体类别消除分组,生成段落或保留独立块,并诊断不合法的混合内容。各前端的块语法边界由自身定义,片段校验共用后端规则。
Call.comptime 保存编译期实参;Call.args 按源码顺序保存 CallArg.Positional 或 CallArg.Named,两者的值均为 Expr。具名参数在签名绑定后才成为字段;重复参数保留到 Resolve 诊断,恢复树采用后一个具名值。调用目标与每个参数分别保留源码范围。
注解不决定目标,也不写入内容节点:前端只记录 Annotation { dict: LiteralDict, span, kind },与 forest 一起交到共享后端。LiteralDict 保留只允许数据字面量的 Expr,Resolve 将其转换为 ResolvedAnnotation 中的 Dict,不执行任意调用;模块注解使用相同规则解析到文档根属性。Shape 完成后由 Materialize 按 span 挂载;Ref 没有自身节点,不能承载节点注解。
RExpr 是独立的已解析内容节点结构,保存 ctor、已知 Value 字段、输入 children、body flavor、attrs 与源码范围;pending 保留已检查的调用、内容插值或失败信息。Shape 只使用公开内容契约,原样保留待求值表达式;Materialize 在 Shape 之后按源码位置挂载注解,将 attrs 与其他已解析信息写入 Item,并填充来源。
ImportKind 区分 Module { alias }、Members(Vec<ImportMember>) 与 Wildcard;ImportMember 保存原成员名、本地名称和独立的原始源码范围。宿主加载依赖后,Context.importbindingsresolved 按公开表展开选择并统一检查冲突;作用域中的 ImportBinding 保存 ModuleId 或 DefId,并由 ModuleScope 保留名称拥有关系。源码声明与已解析绑定分别保存,见 定义身份。
独立 Markup 支持文件级 #import;原生与 Markdown 前端都输出相同 Import 模型,声明不进入内容森林。Code 字面量禁止声明导入。宿主先读取声明并装配作用域,再调用共享后端。
后端以具名调用为入口,Code 实例支持词法绑定和表达式检查。独立 Markup 禁止内容值引用;Code 字面量允许按静态内容类别插入森林。引用不被改写为无参调用。
语法域与树的组织
notist-syntax 只负责词法、语法树、源码范围与语法诊断,不依赖 core 或 pipeline。markup 与 code 模块分别拥有各自的 AST、lexer 和 parser,共用 tree 模块的 Rowan 类型及根导出的 Parse / Diagnostic。文件级导入复用 code.parseimportprefix;调用头和注解的前缀入口分别为 code.parsecallprefix 与 code.parseannotationprefix;返回的 Parse.consumed 表示已消费字节数,后续正文由调用方继续处理。code.parsecallheader 与 code.parse_annotation 用于要求完整消费输入的独立片段。
Document 与 Module 是不同的主树根,分别由 markup.parsedocument 与 code.parsemodule 构造;类型化 AST 是对应 CST 的访问视图。Markup 调用头与注解的 Code 片段节点会接入 Markup 主树。
Code 的内容字面量采用另一种边界:主 CST 中的 ContentLiteral 只包含括号与不透明 ContentText;具体前端的内容树保存在 Parse.contents,通过源码范围关联。一份 Parse 可关联多棵内容树。内容树使用自己的表示,原生内容采用 Rowan,Markdown 内容采用 rushdown。
内容字面量的委托解析
Code 的内容字面量通过 notist_syntax.content.ContentParser 委托给所选前端:
;
前端负责识别结束括号、内部不透明区域及错误恢复,返回 ParsedContent。接口通过源码与 open 字节位置委托,不暴露 token cursor 或 builder。
字段 | 契约 |
|---|---|
range | 从开括号开始;闭合时包含结束括号,未闭合时延伸至恢复终点 |
body | 去掉开括号及已存在的结束括号;保留正文原文 |
flavor / closed | 内容的 Inline / Block flavor,以及结束括号是否存在 |
syntax | Any + Debug 的不透明前端解析产物 |
diagnostics | 字面量解析产生的语法诊断 |
range、body 与 diagnostics 的范围相对于传给 parse_content 的源码;前端树的内部坐标由其表示决定。两种内置前端的内容树均使用正文局部坐标:原生内容为以 Document 为根、只包含 body 的 Parse,Markdown 内容为 source 等于 body 的 MarkdownSyntax。
Code 解析器按需扫描 token,读取前端返回的内容范围后恢复 Code 词法;内容正文按所选前端的规则识别。
Code 阶段一通过 ContentParser 解析内容边界与语法,再通过 ContentLowering 将 ParsedContent.syntax Lower 为 IR₁。Expr.Content 保存 Expr 森林、注解 side list、body flavor 与源码范围。原生内容采用 Rowan Parse,Markdown 采用 MarkdownSyntax;前端树只用于本次 Lower,不进入 Code 语义模型。
阶段二将 IR₁ 与当前 Code 实例的词法环境交给共享 Markup 内容管线,完成 Resolve → Shape → Materialize,直接返回 Item 森林并嵌入 TypedExpr。该管线从 IR₁ 之后开始,不包含前端 Parse / Lower。内置前端在第一阶段就将嵌套表达式、注解与诊断还原到原始源码坐标。
Pipeline.analyzecontentliteral(dialect_path, source, context) 按扩展名选择前端,消费一个内容字面量前缀,返回 ContentAnalysis 和 Parse。ContentAnalysis 保存不带 Doc 根的 Item 森林、body flavor 与合并的解析及内容编译诊断;Parse 保留 Code CST 和关联内容树。该入口不执行内容实现,后续 Code 源码留给调用者。
code.parse_module 与嵌套调用头共享 ContentParser 边界协议。直接调用的 children 内容槽同样保存包含 IR₁ 的 Expr.Content;实例检查期间由共享后端按父调用的内容契约处理结构轴、Shape 与注解。
默认实现与注册
前端实现 | 实现位置 | 默认扩展名 |
|---|---|---|
NotistFrontend | notist_pipeline.frontend.markup |
|
MarkdownFrontend | notist_pipeline.frontend.markdown |
|
Pipeline.default 注册两种内置前端。Pipeline.new 创建空注册表;with_frontend 接收同时实现 Frontend 与 ContentLowering 的具体值,以 PipelineFrontend Trait 对象保存。PipelineFrontend 提供 blanket impl,外部前端实现所需接口即可注册,pipeline 不静态依赖外部实现。扩展名重叠时最后注册者优先;每次源码处理只调用选中的前端。
use ;
let native = new.with_frontend;
let markdown = new.with_frontend;
let both = new
.with_frontend
.with_frontend;
Frontend 及其输出类型从 notist_core.frontend 导入,Expr、Item、Registry 与诊断也从 notist-core 对应模块导入。notist-pipeline 公开自身的 Pipeline、分析结果与处理入口,前端注册表位于 frontend 模块。
前端注册表是 Pipeline 的内部实现。Pipeline.analyze(path, source, context) 与 inspect(path, source, context) 共用完整后端;宿主需要先加载 import 时,使用 lower 与 analyze_lowered 分开执行同一流程。前端不读取 Context;它从 Resolve 开始参与检查。装配边界见 文档管线。
两种内置前端均由 notist-pipeline 的 frontend 模块提供。顶层 notist 也公开 Pipeline、NotistFrontend、MarkdownFrontend 及共同的 Frontend 类型,供 Vault 宿主选择和注册前端。扩展名只用于注册器选择实现,同一前端可服务多种扩展名。
原生解析与 lowering
NotistFrontend 位于 notist_pipeline.frontend.markup,组合以下过程:
.not source
→ notist-syntax.markup.parse_document
→ Parse(Document 根与关联内容树)
→ notist-pipeline.frontend.markup.lower_document
→ Expr 森林 + 根属性 + 诊断
notist-syntax.markup 拥有原生正文的 lexer、parser 与 AST 视图,自行维护游标与 builder;调用头和注解复用同 crate 的 code 片段入口。pipeline.frontend.markup 负责 Document → IR₁ lowering 与 Frontend 的组合,将解析诊断和 lowering 诊断合并为 FrontendOutput。共享 CST 类型是原生语法的实现选择,不是内容接口的要求。
lower_document 的输入边界如下:
let parsed = parse_document;
let mut diagnostics = Vecnew;
let =
lower_document;
这里 diagnostics 收集 lowering 诊断;parsed.diagnostics 由解析调用者持有。前端承担源码处理的组合,lowerdocument 消费 Parse 中的 Document 与关联内容树。共享入口 callheader 同时接收调用 AST、所属 Parse 和 ContentLowering,以复用嵌套内容的解析产物;annotation_dict 与 literals 消费各自的语法节点。
.notc 声明由 notist-syntax.code.parsemodule 产生 Module AST,进入 Pipeline.compilemodule → CodeModule → Context 的装配路径,见 内容函数与输出绑定。该路径复用字面量转换,产物包含公开签名与函数体。
Markdown 与显式 Notist 语法
MarkdownFrontend 通过 rushdown 解析 Markdown AST,并将其 lower 成同一种 IR₁。注解与显式调用由 rushdown 扩展解析器识别;它们与标题、列表、强调等 Markdown 节点共同进入 lowering。调用 body 内部产生的注解与 forest 一起上浮,并按 body 的源码偏移映射到外层文档。
注解与调用头分别使用 notist-syntax.code.parseannotationprefix 和 code.parsecallprefix 直接解析,不经过原生文档入口,再复用 notist_pipeline.lowering 中的属性、调用参数和字面量转换。调用的 [body] 源码由 Markdown 前端按对应 flavor 解析并 lower 为 children;引用、列表等容器移除的源码前缀通过位置映射还原为原始字节范围。
两种正文语法的归属为:
所在前端 |
|
|---|---|
NotistFrontend | Notist Markup |
MarkdownFrontend | Markdown,包含该前端的 Notist 扩展 |
[body] 为行内内容槽;两端内侧均留空白或换行的 [ body ] 为块内容槽。#node[body] 是显式透明内容调用,正文仍使用所在前端的语法。#f([body]) 这样的普通内容参数不支持,内容通过调用后的唯一 children 槽挂载;规则见 Types 和 Markdown Syntax。
参数字符串、代码和数学 payload 保持不透明;其中的调用与注解不会再次执行。Markdown 的普通文字与链接字段按其解码规则转换,显式调用的字面量按 Notist 字面量规则转换。
解析产物与调试
FrontendOptions.capture_syntax 默认 false,控制解析产物是否随编译结果保留。正常 compile 完成解析和 lowering,返回 IR₁、根属性及诊断;解析树在本次处理结束后释放。选项为 true 时,前端可把已生成的 CST / AST 放入 FrontendOutput.syntax,供工具继续读取。Code 阶段二只需要已 Lower 的 IR₁;前端内容语法树随 Parse 的生命周期保留,不随 CodeModule 保存。
FrontendSyntax 是 Any + Debug 的不透明解析产物接口,具体 AST 类型由前端决定。现有实现保留:
NotistFrontend:notist_syntax.tree.SyntaxNode。
MarkdownFrontend:notist_pipeline.frontend.markdown.MarkdownSyntax,包含 source、rushdown Arena 和 root。
use ;
use ;
let output = MarkdownFrontend.compile;
let syntax = output.syntax.as_deref.unwrap.as_any
..unwrap;
assert_eq!;
Pipeline.analyze 使用默认选项。Pipeline.inspect 请求捕获解析产物,并在同一次处理过程中保存 Inspection.syntax、lowered 与 shaped:分别为前端解析树、IR₁ 和完成 resolve / shape 的 IR₂′。捕获选项影响解析产物的保留,不改变文档语义或诊断。
工具按具体前端类型读取解析产物:cst_json 的 tree / ast 字段对应原生 SyntaxNode,MarkdownSyntax 由工具单独处理;ir1、ir2 与 core 为所有前端共用的表示。
文档扩展
FrontendOutput 的静态 imports 先由宿主加载,表达式森林、注解与根属性随后组成 LoweredDocument,交给 before_check。扩展在共享表示上工作,两种前端复用相同的阶段调度;前端自身不调用扩展。inspect 中的语法产物对应原源码,lowered 捕获扩展后的 IR₁,见 文档扩展。
导入预览与编辑器导航
preview.source_imports 与同名 WASM 接口返回 imports 和 diagnostics。每个导入包含 path、kind、start、end;kind 为 module 时另含 alias,为 members 时另含 members 数组(name、alias、start、end),为 wildcard 时由宿主加载模块后展开。范围均为原始源码的 UTF-8 字节范围。浏览器依赖发现仅使用 path,所有选择形式共用同一规范资源图。Code 的 CST / AST JSON 保留 ImportMember 子节点及其范围。
Pipeline.definition_at(path, source, context, offset) 在已加载源码快照的 Context 中解析显式调用目标和成员导入位置。Code、原生 Markup 与 Markdown 均复用降低后的调用表达式;Code 中的参数、编译期参数、children 与 let 绑定参与词法遮蔽。LSP 用这一入口提供悬浮和定义跳转,目标为原定义文件与范围,成员重命名不改变目标。LSP 在未保存的资源覆盖视图中加载同一模块图,缺失成员与导入冲突诊断归属声明所在文件。