解析器生成器还有人写?这个 C++ 新项目把「先建树再跑语义」做回来了
Hacker News 前排刷到一个 Show 帖,三十个 points、十五条评论,作者一个人把解析器生成器整个重新搬了出来。项目叫 Yantra,一个面向 C++ 的 LALR(1) parser generator,名字看着陌生,但戳中的还是那套做编译、做 DSL 都绕不开的问题:解析器到底该怎么写,语义动作到底该什么时候跑。
Yantra 的核心卖点一句话讲清楚:它把 lexer、parser 和 AST walker 都从一份 grammar 生成出来,顺序是先把整棵 AST 建完,再单独做一趟自上而下的遍历。这跟传统工具不一样。Yacc、Bison、Lemon 这些主流 LALR 生成器都是边归约边执行你的语义动作,bottom-up 一路推进,一条规则 reduce 的时候,你还不知道它的父节点长什么样。结果是很多 grammar 被迫去写手搭的 AST 类,后面再单独补一趟遍历,只为了能在看到更多上下文之后再下决定。
Yantra 给的模型反过来了:整棵树先存在,然后再走。因为树是全的,父规则的 action 可以在它子节点被访问之前就执行。README 里那个例子很直观,「1 + 2 + 3」跑出来是先打 "Adding",再依次打三个 Number,最外层也就是树根的 "Adding" 最先出现,这在边归约边执行的模型里做不到。
它还允许一份 grammar 定义多个 walker,比如同一份 parse 既吐 C++ 又吐 Java,AST 和 walker 类都是生成出来的。集成 lexer 带 mode 支持(能处理嵌套注释),有可选的单文件 amalgamated 输出模式并生成 main(),C++23,MIT 协议。版本号很诚实:0.5.1,pre-1.0,单维护者,作者在帖子里直接说「我更想知道哪里会坏,而不是让它看起来比实际更成熟」,已知限制列在仓库文档里。
评论区吵架的焦点恰恰是「这年头还有必要写新的 parser generator 吗」。第一个回复就说,绝大多数编译器早就settle 在递归下降 / precedence climbing 上了,包括当时也在前排的另一个帖子。顺着这条线,讨论一路挖到核心问题:为什么生产级编译器(Clang、rustc、Go)都转手写递归下降?最大的原因是错误信息。
这是老问题。手写 parser 可以精确地说出「return 语句后缺了分号」,因为它确切知道自己在处理哪个结构;而生成出来的 parser 的错误通常是从状态机机械推导的,一句「expected one of: X, Y, Z, got W」——正确,但泛泛。作者自己也承认,这事目前基本还是这样,只有少数工具在错误容忍上有真进展,tree-sitter 是最好的例子,它就是为了从坏输入里做出尽力而为的树而设计的,这也是它适合做编辑器后端的原因。ANTLR 也有可配置的恢复策略。
Yantra 这边更坦白:目前完全没有错误恢复,一个语法或 lexer 错误就直接停在那个位置,不做重同步,不在一趟里继续找更多错误,这是文档里明确列出的已知空缺。作者说对这种面向的小型或演进中的 DSL 来说,这个权衡大概可以接受,但它确实存在。
评论里把「手写 vs 生成」和「自顶向下 vs 自底向上」这两对概念拆开了。手写 parser 几乎都是递归下降(一种 top-down),最流行的生成器(yacc、bison)是 bottom-up,但顶向下的生成器也存在(ANTLR、Coco/R、Chomksy)。作者做的这事有意思的地方在于:递归下降本身就是自顶向下的,所以「先建树再决定怎么处理」天然是白送的,等于 ANTLR 的 LL(*) 也这样。而 Yantra 是在保持 LALR 那种表驱动的 bottom-up 解析的同时,把这项能力给拿到了。
所以它真正适合的领域不是又要写一个巨型生产编译器,而是小一点的、还在演化的 DSL——你想要一份可读的、声明式的 grammar 作为规范,自动做冲突检测,而不是手调 lookahead 逻辑;以及想从同一份 grammar 生成多份输出(比如 C++ 和 Java),这种东西硬挂到手写的 parser 上后期会很难受。评论区也有人顺着这点建议,如果将来给 AST 和 walker 类加 Python 绑定,做「从 Python 高速解析结构化数据」会很好用,作者还没答应,但方向挺对。
这个帖子真正让人觉得有价值的,倒不只是那份 DSL 愿景,而是作者对边界的清醒。他反复强调错误恢复没做、树是后建有成本、这是个单维护者的 0.5 版本。在「工具还没看起来成熟就先承认哪里会坏」这件事上,它比很多一上来就画大饼的 Show 项目可信得多。要不要真的试,取决于你手头有没有一个正在变难维护的手写 parser 或者想重新表述的 grammar——有的话,去 README 的 Quick Start 花几分钟跑通一个解析器再决定,也不亏。毕竟这一行里,最近的大伙儿都一股脑在递归下降上,有人在 LALR 这条老路上把「先建树」捡回来,本身就值得看两眼。