距离上一次 v0.3 已经快两年过去了。由于是业余时间随机写的,所以进度很慢,这中间,去玩了半年的怪物猎人,然后又开始玩其他的游戏,以及最近的塞尔达,所以其实不是那么多时间。最重要的就是我有 代码洁癖,所以代码是整理了又整理,重写又重写,所以跟上个版本相比,就是从头到尾彻底的重新写了一遍。

进度慢的另一个重要原因当然也是正确性的强迫症,基本上每重写一个函数,都需要全局的考虑,通常需要在脑子里把所有可能性都过一遍,确保正确性。有的需要用数学归纳法证明程序的正确性,也全都自己过一遍,所以编写的过程是非常漫长的。

还有就是它不是 8cc 那种玩具,是实实在在的产品,前几天我自己从 http://ftp.gnu.org 下载编译了几个程序,包括 binutils, bash, m4, ed 等,都可以编译通过,而且速度相当快。当然这中间也发现了两个bug。一个已经修复了,另一个也了解怎么修复了,只是在想有没有更优雅的做法,所以暂未修改。

v0.4 一开始只是想把上一版的后端代码生成器换了,以及重新写一个预处理器。预处理的正确性当然必须保证,更重要的是性能是以最新的 gcc 为目标,至少要跟它差不多吧。我测试发现预处理速度最快的是 tcc,当然其他方面如兼容性、正确性以及信息的完整度都不如 gcc,而且最新版的 gcc 确实已经很快了,clang 还要稍微慢点。

预处理器貌似简单,其实有很多大坑。最典型的就是空格的处理,看一下 The GNU C Preprocessor Internals 就知道了。估计也就 gccclang 能够实现完整,毕竟是工业界产品级的编译器。即使像微软最新的 Visual Studio 2017 也是不行的。

至于后端,一开始想直接采用现成的方案,也就是 lcc 用的 BURG 那一套,代码量少,很快就能全看完,然后就可以照搬一个,是个代码生成器的生成器。其内部是采用动态规划,加上代码模板以及每个模板的代价,这样自动生成的。然而它有几个缺陷,最大的问题是不够简单。它首先需要额外写一个 burg 程序,然后读取一个类似 yacc 那样的输入文件,然后产生一个代码生成器的源文件,这个自动生成的源文件就随着编译器其他源文件一起编译成最终的程序。这跟我最初的目的是不一致的,我希望整个程序的 BUILD 特别简单,不想编译出两个程序,代码文件越少越好,可执行文件最终就一个就好。第二个缺陷就是,它不是彻底可变目标的,lcc 没有 x86_64 target,除了可能没人维护之外,最大的原因我觉得还在于这个后端,它是可变目标的,比如可以 相对较快 地支持新 taget,但又不彻底。如果想把它迁移到 x86_64 这么奇葩的平台,真是有点费劲,需要做很大的改动才行。

但是 lcc 的后端并非全无可借鉴处,它的寄存器分配就相当不错,既做到了简单,又没有特别差劲,至少比那些玩具编译器强了好几个等级。当然,我又发现其中有个很致命的 bug,只不过这个 bug 在 32 位平台不容易复现罢了,但是专门写程序复现应该是可以的。

gcc 的代码我就想吐槽了,非常的凌乱。首先它用 autoconf, automake 这个就值得吐槽一遍了。这玩意就是专门讨好编译安装软件的人的,对开发者、源码阅读者那是相当的不友好,而且 automake 这一套也是有缺陷的,譬如 -fPIC 这个选项。它就是专门给 gcc知名 编译器特殊准备的。

clang 的代码清晰性相当可观。当然,它用 cmake 这个本身也值得吐槽一遍了。另外,它的编译过程也是非常的漫长。

然而我主要还是用它们来测试,看看它们生成怎样的代码,以及中间表示。不然我觉得单从炫技角度来看,它们全部不如 tcc。但是 tcc 毕竟也有一些局限性,比如可能无法编译最新的代码了,因为功能不完善,有bug。再比如生成代码使用的是 栈顶缓存,并且设了栈的上限。

总之,理论和实践是两码事。细节是魔鬼。所有优雅的模型,到了实践,可能就改的面目全非,一点也不简洁优雅了。同时要用数学的方法来证明程序的正确性,这也是非常关键的,特别是编译器这种程序,不能出差错的。单元测试本身局限性非常的大。是骡子是马还得拉出来溜溜,直接去编译工业界的产品源代码,更加能说明问题、发现问题。

最后,总之,在把已知的问题修复完毕之后,算是告一个段落了。