← 解析器 · 各路引擎、一门文法与编辑器要的那半边 / 容错解析:一次报出全部错误 待审核 20 / 25
panic-mode · 同步集 · advance 不变量

容错解析:一次报出全部错误

前面各页的引擎全是 fail-fast:第一个语法错误即整体失败、只报一个错、不产出树。对编译器前端这是合理默认;对编辑器则完全不可用。

理由很简单:正在编辑的文档大部分时间是不合法的。刚敲下 if ( 的那一刻,语法高亮、括号匹配、补全提示、跳转定义都还得工作,而它们都需要一棵树。语言服务器面对的常态是「有错的输入」,它的 parser 必须在有错时也产出有用的东西。

1 · 容错解析的要件

定义 1.1(容错解析) 容错(resilient)解析器满足三条:错误被收集而非抛出,返回值是 { ast, errors } 而非 { ok };出错处放 error 占位节点,使好的部分仍解析成有效子树;出错后按同步集丢弃 token 重新对齐,以便继续解析后续内容。

第三条即 panic-mode 恢复,是最经典也最简单的策略。本仓库的实现里同步点是分号、下一条语句的起始 token、以及文件结束:遇错就一路丢弃直到撞上其中之一,然后当作新语句重新开始。

图 1-1 · 同一段源码的全部错误与容错 AST,均由 @vega/parsing/recovery 的真实 parse 给出。标⚠的节点是 error 占位。读数条同时给出救回的有效语句数与占位节点数,前者越大、后者越小,容错质量越高。末段按钮实测 400 个随机串全部返回。

2 · 返回值形状即契约

{ ast, errors }{ ok: true, tree } | { ok: false, error } 的差别不只是写法。后者把「成功」编码进类型,调用方必须先分支才能拿到树;前者的树总是存在,「有没有错」是一个独立的查询。

这个形状的选择会传染到整个下游。语言服务器的每个功能都要能在「树可用但带错」的状态下工作:语义高亮跳过 error 节点、补全在 error 节点附近仍给出候选、格式化拒绝在有错时改动那一段。若 parser 的类型强迫「有错就没树」,这些功能就只能各自缓存上一次成功的树,而那份树与当前光标位置已经错位。

3 · advance 不变量

容错解析有一个隐蔽而致命的失败模式:恢复循环空转。若某轮循环既没有消费 token 也没有报错,下一轮会遇到完全相同的状态,循环由此无限持续,最终耗尽内存。

警示 · 这是容错解析实现里最常见的严重缺陷,且不易被例子测出来,触发它需要一个刚好让恢复逻辑原地打转的输入。对策是把它变成一条硬不变量:恢复主循环每轮必须至少前进一个 token。实现上通常是在循环末尾加一个「若位置未变则强制前进」的兜底。同族的问题也出现在 combinatormany 里(若被重复的 parser 成功但零消费,many 会永不停止),本仓库两处都有显式保护。

这条不变量的可测性是它的价值所在:任取随机串喂进去,若能返回就说明没有空转。图 1-1 末段的按钮实测 400 个随机串,全部返回。这是一次典型的 property-based 测试,断言的不是「输出等于什么」,是「一定会输出」。

4 · 恢复质量怎么衡量

panic-mode 简单但粗:它丢弃的可能是半句有用的内容。衡量恢复质量有两个可观测量,图 1-1 的读数条都给了出来。

救回的有效语句数越大越好,它衡量「一处错误污染了多少无关的部分」;error 占位节点数越小越好,它衡量树上有多少区域是不可用的。理想的恢复是错误局部化:一处笔误只让一个最小的子树变成占位,其余全部完好。

panic-mode 在句间错误上表现不错(分号是个可靠的同步点),在句内错误上就差:x = 1 + 里少一个操作数,整条语句都会受影响。下一页的 phrase-level 修复针对的就是这一类:在失配点做最小编辑让解析继续,而不是丢弃整句。

5 · 现实里的极端形态

HTML 是容错解析被推到极致的例子:规范不是「建议实现容错」,而是明文规定了错误恢复的每一步。未闭合的标签怎么补、错位的 </p> 怎么处理、<table> 里的裸文本挪到哪里,都有确定的算法。任何两个符合规范的解析器面对同一段畸形 HTML 必须产出同一棵树。

这条路线的代价是「能解析」与「合法」彻底脱钩:任何字节串都是合法的 HTML 文档,文档的正确性于是无法由解析器保证。相关讨论见 HTML Parsing 系列

Markdown 与 Djot 走的是另一种极端:没有非法输入。它们的 parse 是全函数,返回 AST 而非 Result,因为任何文本都是合法文档。这条设计使容错问题消失,代价是「作者写错了」这件事完全无法被察觉。

6 · 参考文献

  1. Aho, A. V., Lam, M. S., Sethi, R., & Ullman, J. D. (2006). Compilers: Principles, Techniques, and Tools (2nd ed.). Addison-Wesley. §4.1.3(错误恢复策略)。
  2. Kats, L. C. L., de Jonge, M., Nilsson-Nyman, E., & Visser, E. (2009). Providing rapid feedback in generated modular language environments. In Proceedings of OOPSLA'09 (pp. 445–464).
  3. WHATWG. HTML Standard, §13.2 Parsing HTML documents.