D0005: Lowering and Structuring
Notist 使用两步语义管线:Lowering 负责把 syntax 转换为线性的语义 Element sequence,Structuring 再把这条 sequence 组合成 Paragraph、List 和独立 Block。
source
-> syntax Parse
-> lowering and function evaluation
-> flat Content / Element sequence
-> structuring
-> StructuredDocument这两步都属于 notist-eval。它们共享 Content 和 Element model,但职责不同:Lowering 理解语法与函数;Structuring 只理解已经产生的 Element 排列。
Syntax Parse
notist-syntax 保留源码事实和精确 range,包括:
WikiLink。
mode-aware EmbeddedExpression、Content literal 与 CallExpression。
函数名、argument ranges、trailing Content、Attributes 与完整 source range。
recoverable SyntaxError。
Syntax AST 不尝试成为最终文档结构,也不执行函数。它必须适合 diagnostics、formatter、Tree-sitter/LSP 对齐和增量解析。
Lowering
Lowering 将 syntax 解释为语义模型:
普通文本变为 Text Element。
WikiLink 变为 Reference Element。
空行变为 Parbreak。
语法糖变为对应的内置 Element。
EmbeddedExpression 独立投影 Annotation,再求值并插入其结果。
Call 求值 arguments 与 trailing Content,完成 signature binding,再执行 Function。
结果是 flat Content:
Content
└─ Vec<ElementNode>
├─ Text
├─ Reference
├─ Heading
├─ ListItem
├─ Raw
├─ Custom
└─ Parbreak“flat”只表示顶层按执行顺序存放 Element,不表示 Element 内部不能有树。Heading、Strong、Quote 或 Custom Element 仍然可以拥有嵌套 Content。
Sugar Recovery
Heading、list、task 与 pipe table 等高频语法糖需要先从连续源码中识别候选 span,再 lower 成内置 Element。候选并不保证结构完整:列表可能突然 outdent,表格各行也可能缺少尾格。恢复必须单向推进:一个候选 span 失败后,只能在禁用 block sugar 的 fragment 中保留 inline 语义,不能把同一 range 再交给同一个候选识别过程。否则“识别—失败—重新识别”会形成没有源码进展的递归。
可以安全归一化且不改变作者顺序的输入应直接容错。pipe table 以最宽行决定列数并给较短行补空 cell;列表在缩进回落到当前 span 起点以下时结束 span。无法安全归一化的候选降级为普通内容或 diagnostic。无论选择哪种恢复结果,lowering 都必须消费 source range 一次并继续向前。
Function Call
#quote[Inside [[self::target]] and #heading(level=2)[Title].]Trailing Content 是普通 Content literal。Lowering 先递归求值得到 Content,再与其他参数一起完成 signature binding:
CallExpression
-> evaluate arguments and trailing Content
-> bind Function signature
-> Function::call
-> FunctionOutput(Content)Function 不自行重新调用 parser。内部 Annotation 和 diagnostics 在同一 analysis/lowering 过程中保留。Raw text 作为 String 或 host Raw syntax 求值,不形成另一种 call mode。
Annotation Projection
Before #[annotated [[self::target]]]@concept,#important after.... 是嵌入的 Content literal;Attributes 让完整 EmbeddedExpression 同时产生 source Annotation。Lowering 不创建 Annotation 容器 Element,而是继续输出 Text 与 Reference,并由 analysis 保存 scope range 与 metadata。
Annotation 与 Content 正交,所以 Structuring 改变 Element 的组合方式时,source identity 仍保持稳定。更完整的 range 和 Attribute 模型见 vault::designs::D0002-scope-and-attrs。
Why Structuring Is Separate
Lowering 后的 Element 只表达执行顺序:
Text("Intro")
Parbreak
ListItem("A")
ListItem("B")
Heading(level=2, ...)
Text("After")Structuring 根据 Element 的 inline/block 性质和相邻关系建立文档结构:
StructuredDocument
├─ Paragraph(Text("Intro"))
├─ List
│ ├─ ListItem("A")
│ └─ ListItem("B")
├─ Heading(level=2, ...)
└─ Paragraph(Text("After"))如果 parser 直接建立最终 Paragraph/List 树,那么函数、用户代码和未来控制流动态产生 Element 后,还需要复制一套组合规则。两步设计让所有静态语法和动态函数输出都走同一个 structuring pass。
Structuring Rules
Structuring 使用以下基础规则:
连续 inline Element 组成 Paragraph。
Parbreak 结束当前 Paragraph,但不进入最终 document。
相邻 ListItem 组成 List。
Heading 和 block Raw/Custom 作为独立 Block。
block Element 到来前先 flush 当前 Paragraph 或 List。
diagnostics 与 Annotation 原样保留。
Structuring 不重新读取源码、不再次执行 Function,也不负责名称解析或类型检查。它只处理已经 lowering 完成的 Element sequence。
Ownership
notist-syntax
source ranges, mode-aware Parse, calls, attributes, syntax errors
notist-model
Content, Element, Annotation, StructuredDocument
notist-eval
Function runtime, lowering, structuring, eval diagnostics
notist-analysis
modules, names, types, cross-document semantics
notist-lsp / CLI
expose diagnostics and navigation to toolsLowering 与 Structuring 同属 notist-eval 的语义管线。即使编译时间或复用边界促使实现拆分 crate,这两个阶段的职责分界也不随物理包结构改变。
Summary
Lowering 回答“语法和函数按顺序产生了哪些 Element”,Structuring 回答“这些 Element 如何组成可消费的文档块”。Function arguments 与 trailing Content 在 lowering 中统一求值,Annotation 作为独立 source projection 保存。这个分层让解析、类型系统、插件执行和最终文档结构各自拥有清晰边界。