为什么要手写递归下降解析器?我在对比ANTLR后的真实感受

🔑 关键词:递归下降,ANTLR,编译原理,中间表示,手写解析器

📖 摘要:从一个业余编译器爱好者的角度,对比手写递归下降与ANTLR生成解析器的差异,以及学习编译原理的真实体会。

大学时编译原理课用的龙书,当时照着例子写了个计算器,结果连函数调用都调不出来,差点挂科。后来工作第一年参与一个内部DSL项目,技术经理说直接用ANTLR吧,省事。我当时真觉得解放了,因为那个语法足足有四十多条规则,自己手写不得累死?于是我们用ANTLR生成了词法语法parser,还顺手生成了Listener和Visitor骨架。最开始确实爽,遇到语法错误往上一抛就完事。

图片

但是用久了发现事情没那么美。ANTLR生成的tree是Token流再加一层上下文节点,跑一个小配置的解析都要分配好几万个对象,放在我们那个低配Docker容器里GC压力特别大。而且改语法还好,最tm难受的是错误信息。用户写错一个分号,ANTLR默认报像“mismatched input '}' expecting {, ...}”这种话,业务用户根本看不懂。我们后来做了很多错误恢复的二次包装,等于自己写了个解释器套在生成的parser外面,比原来还复杂。后来我做了个失眠夜里的决定:用Python手写一个递归下降解析器来替换ANTLR生成的代码。

图片

手写的感觉真的不一样。一个token一个token往前看,遇到错误的时候你知道当前在哪个函数里,上下文是什么,能直接给出“第14列附近,缺少赋值运算符右侧的值”这样像人话的提示。还有性能,因为只构建自己需要的AST节点,不需要中间那些overhead,解析同样的文件快了差不多五倍。而且依赖消失之后,部署交给别人也省心得多。当然,手写也不是全是优点。语法稍有改动,就得回去改一个函数把栈调通,不像ANTLR改完.g4重新生成就行。但我觉得,如果你经常要解析复杂语言,或者想要精确控制错误和性能,手写递归下降应该是比自动生成更值得考虑的路。

图片

编译原理真的不止是parser那一块。以前我把大部分时间花在词法语法上,觉得能跑就行。后来为了做一个小脚本语言的常量折叠,我才开始看中间表示和优化。有一段时间我天天在折腾SSA和线性扫描寄存器分配,感觉自己半只脚踩进了沼泽。但就是在那些夜晚里,我突然理解了循环不变式外提为什么能省那么多条指令,也理解了为什么静态单赋值能让数据流分析变得漂亮。说句实话,真正让我对编译原理升起敬意的不是文法推导,而是那些关于怎么在有限寄存器里调度好成千上万条中间代码的博弈。

图片

现在我不再迷信工具,也不神话编译器。编译原理说白了就是一门“程序变换”的学问,把人类语言翻成机器能跑的东西。我手写解析器不是因为它比ANTLR高级,而是因为要亲手摸到每个token的边界,踩着语法规则走一遍,才会对语言本身产生直觉。这就像开自动挡久了再回去开手动挡,你会重新理解发动机的转速和档位之间的关系。如果你也在学或者用编译原理,我的建议是别光盯着自动生成器,挑一个简单的语法亲手写一写递归下降,哪怕只支持加减乘除和括号,也能收获很多。那种从零到能跑通一个表达式求值的满足感,是IDE补全给不了的。

图片

🏷️ 标签: