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 tools

Lowering 与 Structuring 同属 notist-eval 的语义管线。即使编译时间或复用边界促使实现拆分 crate,这两个阶段的职责分界也不随物理包结构改变。

Summary

Lowering 回答“语法和函数按顺序产生了哪些 Element”,Structuring 回答“这些 Element 如何组成可消费的文档块”。Function arguments 与 trailing Content 在 lowering 中统一求值,Annotation 作为独立 source projection 保存。这个分层让解析、类型系统、插件执行和最终文档结构各自拥有清晰边界。