别再被《龙书》吓住了:写编译器,可能真没你想得那么难

编译器为什么总被讲得像“武林绝学”
如果你问一个程序员,什么技术听起来最“高冷”,编译器大概率排得上号。它和操作系统、分布式系统一样,天然带着一点学术光环。很多人的第一反应是:这东西太难,得先学自动机、形式语言、语法分析、寄存器分配,再抱着那本厚得能防身的《龙书》啃上几个月,才有资格写出点像样的东西。
这篇出自 James Hague 的老文章,妙就妙在它直接戳破了这层滤镜。他用了一个很形象的比喻:如果一个完全不懂编程的人,刚入门就被塞了一套《计算机程序设计艺术》,那多半不是“高起点”,而是“直接劝退”。他认为,编译器学习材料长期存在同样的问题——书并不差,甚至非常经典,但它们往往铺得太宽、讲得太全,把初学者困在一片知识森林里,结果理论知道不少,真正能跑起来的编译器却迟迟写不出来。
这其实不是编译器领域独有的毛病。过去十几年,很多技术社区都有类似倾向:把一项本来可以通过实践逐步掌握的技能,包装成必须先完成一整套“正统训练”才能触碰的领域。数据库如此,机器学习如此,编译器更是如此。问题在于,学习的第一步不是建立敬畏,而是建立可操作感。你得先写出一点东西,看到输入如何变成输出,看到自己的想法真的能被机器理解,那股成就感才会推着你继续往深处走。
两份材料,拆掉编译器的神秘感
James Hague 推荐的第一份材料,是 Jack Crenshaw 从 1988 年开始连载的《Let's Build a Compiler!》。这套教程在老程序员圈子里几乎是口口相传的“宝藏文档”。它不追求面面俱到,而是非常务实:带你一步步做一个 Turbo Pascal 风格的单遍编译器,边解析、边生成代码,只做最基本的优化。
这种路线今天看起来甚至有点“复古”,但恰恰因为复古,才适合作为入门。它把“先把东西做出来”放在第一优先级,而不是先建立宏大的理论体系。你可以把它理解成编译器世界里的“先做个能跑的最小产品”。对于初学者来说,这种体验非常重要:原来所谓编译器,并不是一座不可攀登的雪山,它也可以是几十页代码、几个递归下降函数、再加上一点点语法规则,慢慢堆起来的。
不过 James Hague 也指出了 Crenshaw 教程的短板:它几乎跳过了内部表示,也就是抽象语法树(AST)。这件事很关键。没有 AST,当然也能做一个编译器,但灵活性会明显受限。你很难优雅地做语义分析、重写变换、优化,后续扩展也会变得别扭。换句话说,Crenshaw 给了你“能造出车”的能力,但没有完整展示现代编译器那套“可维护、可扩展的工程骨架”。
于是第二份材料登场了:Sarkar、Waddell 和 Dybvig 的论文《A Nanopass Framework for Compiler Education》。这篇论文真正打动 James Hague 的,不是某个具体框架,而是它背后的思想——编译器,本质上就是对程序内部表示的一连串变换。你可以把源代码先变成一种树状结构,然后每一个 pass 只做一件小事:规范化语法、消解糖衣、标注变量、转换控制流、简化表达式,再一步步走向目标代码。
这个想法在今天听起来并不陌生,因为它几乎已经渗透进现代编译器设计的主流叙事。无论是 LLVM 的多阶段 IR 管线,还是很多语言实现里的 lowering、desugaring、optimization pass,本质上都认同同一个朴素道理:复杂系统不是靠一个“万能大步骤”完成的,而是靠很多足够小、足够清晰、彼此解耦的步骤拼起来的。
这篇老文章,为什么放到今天依然不过时
有意思的是,这篇文章最初写于 2008 年,却很像在对 2020 年代的软件世界说话。今天学编译器的人,早已不只是一群做编程语言研究的学院派。做 JavaScript 工具链的人会接触 Babel 和 SWC,做前端构建的人会碰到 AST 变换,做数据库查询优化的人实际上也在做“编译”,写 AI 编程工具的人则越来越频繁地处理代码解析、中间表示与重写。
换句话说,编译器已经从一门“屠龙术”,变成了软件工业里到处可见的基础能力。你不一定在造一门新语言,但你可能在写 linter、formatter、代码迁移工具、DSL、模版引擎、SQL planner,甚至工作流系统。它们背后都有类似的结构:读取一种语言,理解它,变换它,再输出另一种形式。这个时候,如果还把编译器理解为只有博士和底层大神才能碰的领域,就未免有点落伍了。
而 James Hague 这篇文章最有现实意义的地方,恰恰在于它把“编译器学习”从学科神坛拉回工程现场。先用 Crenshaw 打开门,再用 Nanopass 建立正确的方法论,这套路径很像今天很多开发者学习复杂系统的最佳策略:先做一个小而丑但能工作的版本,再逐渐引入抽象和架构。比起一开始就追求“标准答案”,这更符合真实世界的软件成长方式。
这也让我想到 Rust、Go、Zig 这些近年颇受欢迎的语言社区。它们之所以吸引大量开发者,不只是语言本身设计得好,也因为社区里流行“从零实现一个小编译器/解释器”的文化,资料更轻、更直接、更偏工程。相比之下,很多传统教材仍然像在培养“编译器理论家”,而不是帮助读者先成为“会做工具的人”。这两者都重要,但顺序不该颠倒。
真正值得讨论的,不是“难不难”,而是“怎么教”
当然,James Hague 的主张也不是没有争议。把编译器说得“不难”,有时容易让人误解成“编译器很简单”。事实上,写一个能工作的玩具编译器,和写一个工业级编译器,中间隔着巨大的鸿沟。错误恢复、类型系统、增量编译、调试信息、跨平台后端、性能优化、工具链兼容,这些都不是读两篇材料就能轻松跨过去的门槛。
所以问题不该是“编译器到底难不难”,而该是“在什么阶段,应该学什么”。如果目标是理解编译器的基本结构,并亲手做出一个最小可用系统,那么很多传统教材的确给得太多了;但如果你要进入工业级实现,寄存器分配、数据流分析、SSA、优化理论这些硬骨头,迟早还是得啃。James Hague 的文章更像是在纠正学习顺序,而不是否定理论本身。
这背后牵出一个更普遍的问题:我们今天到底该如何教授复杂技术?是先给完整地图,再让初学者自己找路;还是先给一条能走通的小径,等他走出信心后,再补上地形学和历史学?我个人越来越倾向后者。因为绝大多数人不是为了成为某个学科的守门人而学习,他们只是想把自己的想法做出来。文章里有一句很打动人的潜台词:技术不是为了技术本身,而是为了实现想法。这种工程观,在今天反而显得稀缺。
如果说还有什么让我感到一点遗憾,那就是许多中文世界的编译器入门资料,至今仍偏少、偏散。大家都知道《龙书》,也都知道 LLVM 很厉害,但真正能让人“今晚开始,周末做完一个小编译器雏形”的材料并不多。这也是为什么类似 Crenshaw 这样的老教程,尽管年代久远,仍然持续被人反复提起。它们像一把旧钥匙,未必最时髦,但很管用。
从 AI 写代码的时代回头看,编译器教育反而更重要了
AI 编程助手正在把“写代码”这件事变得越来越轻松,但这并不意味着底层原理不重要,恰恰相反。当越来越多的人借助大模型生成代码、改写代码、批量迁移代码,理解代码的结构化表示、理解语法树和中间表示,正在成为一种新的基础素养。
你会发现,今天不少 AI 编程产品做得好的地方,往往不是“会续写代码”这么简单,而是它们能在某种程度上理解程序结构,知道怎么安全地重构,知道修改一处会影响哪里。说到底,这些能力和编译器世界并不遥远。编译器曾经被看作少数人的技艺,现在却在悄悄变成每个高级开发者都应该懂一点的通识。
所以,这篇文章之所以在今天仍有力量,不只是因为它推荐了两篇好材料,而是因为它提醒了我们一个经常被忘记的事实:很多所谓“高深技术”,其实只是被讲复杂了。真正的门槛,往往不在技术本身,而在知识传播方式。如果入门路径设计得足够友好,编译器完全可以像 Web 框架、脚本语言一样,成为一项“你也能上手”的能力。
而一旦更多人敢于动手写自己的语言、自己的工具、自己的转换器,软件世界也许会变得更有创造力。毕竟,编译器不只是把代码翻译成机器能懂的话,它更像是一种赋予表达形式的能力。谁掌握了这种能力,谁就多了一种和机器谈判的语言。