Notist
On this page
  1. 文档前端
  2. 接口与输出
  3. 公共 IR
  4. 语法域与树的组织
  5. 内容字面量的委托解析
  6. 默认实现与注册
  7. 原生解析与 lowering
  8. Markdown 与显式 Notist 语法
  9. 解析产物与调试
  10. 文档扩展
  11. 导入预览与编辑器导航

文档前端

文档前端负责源码解析与 AST lowering,输出共享的未解析 Expr 森林(IR₁)。Markup 管线 注册和选择前端,再执行 Resolve → Shape → Materialize;notist-core 定义共同接口及输出类型。

接口与输出

pub trait Frontend: Send + Sync {
    fn extensions(&self) -> &[&str];
    fn compile(&self, source: &str, options: FrontendOptions) -> FrontendOutput;
    fn cache_key(&self) -> Option<FrontendCacheKey> { None }
}

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 委托给所选前端:

fn parse_content(&self, source: &str, open: TextSize) -> ParsedContent;

前端负责识别结束括号、内部不透明区域及错误恢复,返回 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

.not

MarkdownFrontend

notist_pipeline.frontend.markdown

.md、.markdown、.notmd、.nmd

Pipeline.default 注册两种内置前端。Pipeline.new 创建空注册表;with_frontend 接收同时实现 Frontend 与 ContentLowering 的具体值,以 PipelineFrontend Trait 对象保存。PipelineFrontend 提供 blanket impl,外部前端实现所需接口即可注册,pipeline 不静态依赖外部实现。扩展名重叠时最后注册者优先;每次源码处理只调用选中的前端。

use notist_pipeline::{
    Pipeline,
    frontend::{markdown::MarkdownFrontend, markup::NotistFrontend},
};

let native = Pipeline::new().with_frontend(NotistFrontend);
let markdown = Pipeline::new().with_frontend(MarkdownFrontend);
let both = Pipeline::new()
    .with_frontend(NotistFrontend)
    .with_frontend(MarkdownFrontend);

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 = notist_syntax::markup::parse_document("@(id: \"intro\")\n= Title");
let mut diagnostics = Vec::new();
let (forest, module_attrs, annotations) =
    notist_pipeline::frontend::markup::lower_document(&parsed, &mut diagnostics);

这里 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;引用、列表等容器移除的源码前缀通过位置映射还原为原始字节范围。

两种正文语法的归属为:

所在前端

#f(args)[body] 的 body

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 notist_core::frontend::{Frontend, FrontendOptions};
use notist_pipeline::frontend::markdown::{MarkdownFrontend, MarkdownSyntax};

let output = MarkdownFrontend.compile(
    "# Title\n",
    FrontendOptions { capture_syntax: true },
);
let syntax = output.syntax.as_deref().unwrap().as_any()
    .downcast_ref::<MarkdownSyntax>().unwrap();
assert_eq!(syntax.source, "# Title\n");

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 在未保存的资源覆盖视图中加载同一模块图,缺失成员与导入冲突诊断归属声明所在文件。