是的,这又是一个教你写个编译器的教程。

你可能已经见过无数个教你写编译器的教程了,但它们也许或多或少存在各种问题,导致你看了它们之后,依然觉得无从下手:

  • 教程并非面向“零编译器基础”的人群,理解起来比较费劲。
  • 只教了怎么做一个编译器,但没教为什么这么做,读完之后还是觉得一头雾水。
  • 开始编码之前,需要复杂的环境配置,开局劝退。
  • 上来先做和“编译一个程序”这件事八竿子打不着的东西,跟着教程学一个月才能看到自己的编译器编出一个能跑的程序,反馈周期过长。
  • 最终写出来的只是个解释器,或者不能编译到真实的 ISA(例如输出了某种字节码),或者高度依赖其他框架(例如 LLVM),让人体会不到那种“我真的写出个编译器”的成就感,以及“所有事情都由自己实现”的酣畅感。
  • 实现的编译器只能编译一些过于简单的程序,比如排序算法等等,看起来非常无聊。谁会没事干排序玩啊?
  • 教程及其配套资源非中文,或需要付费,且不开源。

研究生期间,我一直在担任本科编译原理课程的助教。为了让本科生更好地理解编译器工作的原理,在参考多个其他教程之后,我设计了一套全新的,教你从零开始写个编译器的教程:

  • 你可以使用 C/C++/Rust 开发你的编译器,教程只要求你具备使用这些语言编程的能力,不要求任何编译原理的基础和相关实践经验。
  • 教程除了告诉你要如何写一个编译器,还会告诉你为什么这么做,以及除此之外还能怎么做。
  • 教程采用增量式、迭代式的思路,引导你从一个只能处理 main 函数的编译器开始,逐步扩展实现一个能处理包括控制流、函数调用、数组在内的,类 C 语言的编译器。编译器可以输出 RISC-V 汇编。
  • 提供基于 Docker 的实验环境,一键配置。环境中还附带自动测试脚本,一键测试你写的编译器。
  • 教程介绍了相关工具,可以为你代劳一些枯燥的苦力活,比如生成 lexer/parser,解析和生成 IR 等等。剩下的事情,如构建 AST、语义分析、生成 IR 以及生成目标代码,完全由你自己编程实现。
  • 你最终实现的编译器足以编译很多复杂的程序,例如 Mandelbrot 集绘制程序,或者 Lisp 解释器(这个解释器甚至可以解释另一个 Lisp 解释器)。我们会提供这些程序,让你用你写的编译器体验到最真实的快乐。
  • 教程及其配套工具、实验环境等,全部开源,完全免费!

你可以从 GitHub Pages 上访问到这个教程:北京大学编译实践课程在线文档。如果你之前从来没有尝试过自己写一个编译器,不如现在就来试一下吧!

我的愿望是,让每个人都感受到写编译器的快乐。

由于教程是开源的,如果你在阅读教程或随教程实践的过程中发现任何问题,欢迎在 GitHub 的相关仓库中反馈,我们会尽量及时处理。

如果你希望在教程相关的内容中贡献自己的力量,欢迎向相关仓库发起 pull request。

标签: 编译原理, 编译, 教程

已有 6 条评论

  1. Seddon Seddon

    膜大佬!

  2. TQL!

  3. anson anson

    大佬,请问这个系统可以提供多一点测试用例吗?

    1. 暂时不会新增测试用例了,可以参考文档中提到的一些第三方测试用例:https://pku-minic.github.io/online-doc/#/misc-app-ref/environment?id=%e4%bd%bf%e7%94%a8%e5%85%b6%e4%bb%96%e6%b5%8b%e8%af%95%e7%94%a8%e4%be%8b

  4. gulugulubing gulugulubing

    https://pku-minic.github.io/online-doc/#/misc-app-ref/riscv-insts, 这里ra的保存者似乎应该是被调用者

    1. 并不是,ra 是 caller-saved 的。

      假设 callee 自己不会调用任何其他函数,也就是说 callee 是个 leaf function,那么它就完全不需要 push ra,返回的时候直接 jr ra 就行。而 non-leaf 会调用别的函数,所以一定会覆盖 ra 的值,因此必须在调用之前 push ra,也就是说在 caller 里保存 ra 的值。

      你可以这么理解:很多 RISC 架构(包括 RISC-V、ARM、MIPS 等等)里,设置返回地址寄存器(或者有的架构叫链接寄存器,其实是同一个东西)的目的,就是消除 leaf function 保存返回地址的开销。

      返回地址寄存器本质上是返回地址栈最顶端那个地址的 cache。同样调用一万次 leaf function,RISC 因为有这个 cache,这些 leaf 都不需要因为保存返回地址而写内存,总共写 0 次内存。但如果是 x86 那样,call 的时候自动 push 返回地址,那每次调用 leaf function 都会写一次内存,总共写一万次。

添加新评论