探索coolc编译器(一)

~引言~

  cousera上的compiler课,是斯坦福cs143的精简版。其中用到一门适合写编译器的小语言,叫做cool。我曾花了3个月的时间跟完了这门课,完成了直到语义分析为止的代码作业,自以为学了个半懂,2年后又一次回顾,才发现自己不懂的依旧太多太多。在这个小小的编译器里,隐含了太多的东西。

~从git与mingw开始~

  我最初的设想,是把coolc的开发环境搬到windows下,这很快就"成功"了。因为斯坦福的达人们提供了一个下载包student-dist.tar.gz。要求使用类debian的OS,而且必须把文件解压至/usr/class。如果遵守这些,那我就不是我了,折腾才是正路。一开始我只是简单的修改了Makefile里的class路径,虽然很多开发依赖并没有解决,跨平台带来的问题此时我也没有预料,但是在make lexer的时候,我成功了。   于是我便想把这个工程托管到github上,可很快,我便遇到了第一个问题——symbol-link。Windows 本身是支持符号链接的,可mingw默认不支持这东西。所有的符号链接都变成了文件copy,这导致整个工程目录很大很冗余,直接上传github会很难受。出于优先删掉大文件的思想,我找到了罪魁祸首的脚本link-object,少许改动,便把该删的东西都删光了。

~不一样的权限~

  当我在linux上把code从github上pull下来的时候,我才知道:权限与权限是不一样的。git存储是包括文件权限的,而windows是不包括这些权限的。"./link-copy"无法运行,非得先chmod +x才行,对于这个问题我又单独写了一些脚本文件,
  #通配符不支持正则,欲匹配无后缀的脚本文件,只有先全匹配*,再去掉*.*
$ chmod +x class/cs143/cool/bin/.i686/*
$ chmod +x class/cs143/cool/bin/* && chmod -x class/cs143/cool/bin/*.*
$ chmod +x class/cs143/cool/etc/* && chmod -x class/cs143/cool/etc/*.*
  trap.handler的问题也是很糟糕的,这是因为斯坦福的教授们在他们所改写的spim程序(binary file)中引用了一个绝对路径 /usr/class/cs143/cool/lib/trap.handler 我又没办法反编译他们的spim(这是一个支持了jna等扩展指令的MIPS模拟器,真希望有源码!)。不得已只有做了一个符号链接
$ sudo mkdir -p /usr/class/cs143/cool/lib/
$ sudo chown $USER /usr/class/cs143/cool/lib/
$ ln -s `pwd`/class/cs143/cool/lib/trap.handler /usr/class/cs143/cool/lib/trap.handler

~跨平台的苦果~

  mingw与linux终究是不同的,最大的区别,叫做
cannot execute binary file
  好吧,这真的是一件较难解决的问题,而且祸不单行,还有一个区别叫做
/bin/csh: bad interpreter: No such file or directory
  第一个问题的原因,是原本的开发环境里预置了四个可执行文件(限linux下),供开发时当作调试用的临时实现(stubs)。解决方式也很简单,就是以"依赖式开发"代替"补完式开发",我觉得自己对自己的可执行文件负责是再正常不过的事情,为什么非得使用预编译好的binary file呢?   而第二个问题的本质原因,是斯坦福的教授们没有跨平台的意识。明明没有用到csh的特性,还非得写个#!/bin/csh -f,坑死人不偿命。在弄清楚各个shell的关系之前,跨平台这件事简直就是天方夜谈。我可怜兮兮的跑去google各个shell的关系,然后得出一个所有shell理应都兼容bourne shell的结论。如此一来所有脚本都写成#!/bin/sh就行了。可惜世事比我想象的复杂,改了头声明就得改脚本内容,[]、[[]]、()这种no zuo no die的syntax区别让人咋舌。   

~Makefile袭来~

  我怎么都不觉得默认的Makefile好,因为它生成的东西实在太过“混乱”。我并不懂.d文件是什么东西,也不明白为什么要include这些.d文件。总之,不懂的实在太多,在大量阅读了Makefile手册之后,我明白了${CFIL:.cc=.o}只是类似宏文本替换的一堆.o,明白了-开头代表忽略脚本中的error,明白了.c.o这种双后缀规则是过时的。最初,我去掉了include,然后手动添加了一行 ${OBJS}: ${SRC} ${TSRC}。翻译过来就是类似这种多对多依赖:
parser-phase.o utilities.o stringtab.o : parser-phase.cc utilities.cc stringtab.cc
  一天后我才发现了多对多依赖的局限性,每次改动一个源文件都导致所有.o文件重新编译;怪不得Makefile要引入了-include。真正想要的依赖是这种才对:
parser-phase.o: parser-phase.cc something.h
utilities.o : utilities.cc something.h
stringtab.o : stringtab.cc something.h
  正是SED大法显威的地方,于是,我又改回了include,而且针对依赖问题改了又改,最终两整天代码行数不超过10行,工作效率低爆了。为了理顺Makefile的依赖关系,我找到一份makefile2dot的python脚本,可是它只能追溯显式依赖,对隐式同名的东西就无可奈何,比如%d与${CFIL:.cc=.d}的关系。有时间的话,我自己写个升级版好了^^

~重现江湖的Bug~

  由于之前的编译器作业是,lexer、parser、semant、cgen分别开发,相互之前并不依赖,导致很多bug得以隐藏于测试用例的缝隙中。在我的新框架下,此种bug顿然现形。比如parser中把dispatch表达式的类型设成了Self,注意不是self,这种问题只有在semant的时候才可能检查出来,可是cs143的semant开发并不依赖自己的parser,这就是bug存身之处啊。   另一种bug是跨平台时才发现的,称为Windows下的独有bug,比如可恶的0x1A(这实际上是Ctrl-Z),平白无故就能导致一个<<EOF>>。只有把open(file,'r')改成open(file,'rb')才能解决。这个鸟问题的深远影响远不止如此,mintty与rxvt里Ctrl-D与Ctrl-Z都无法等于<<EOF>>(MinGW bug),让人根本无法触发一个EOF信号,极为痛苦。(norxvt时可以Ctrl-Z,Enter。)

~古老的perl~

  perl是一门古老的语言,与我同岁,其语法实在与现代语言相去甚远,故以古老称之。coolc的测试脚本是perl写的,而我在mingw里执行perl的效率奇慢无比,我不知道为什么,我怀疑mingw里perl的文件读写有性能问题,可惜查无实据,问题也并没有解决。   另一个问题也是perl引起的,叫做pack/unpack。由于测试脚本中包含了一段特殊编码的文本,可以用unpack('u', $line)解压成一个tar.gz文件,再用tar解压成一堆测试用例。这里的'u'不是unicode,也不是utf8,而叫做uuencode。我特意去查了一下这种编码的原理,原来是利用了3x8=4x6,取连续的3字节编码转换成4字节(8-bit到6-bit)的可打印字符,编码效率还挺高的。   问题在哪儿呢?问题就是我要修改的脚本文件全部隐含在uuencode编码后的文本里,导致我必须修改脚本文件->打包tar.gz->pack->修改.pl文件。而对于那步关键的pack,我表示不会perl。可问题总有解决方法,我用python写了一个同样功能的小程序,解决了pack这一步。
import uu

with open('grading.tar.gz', 'rb') as rfile:
    with open('grading.tar.gz.uu', 'wb') as wfile:
        uu.encode(rfile, wfile)

~强测试、撞墙~

  在细读了.pl测试脚本之后,我发现这些测试很弱,行号、文件名、nearby之类的问题均被屏蔽。利用上述方法修改.pl文件中隐含的PA?-Filter文件后,我利用-r参数强制开启了强测试。无数原本满分的代码变得傻叉,主要是因为行号。cool里的parser行号定位均定位在最后一个字符,这导致了我对cool.y文件的大量修改。nearby问题是真正困难的问题,bison里error的用法太奇葩,我根本不懂什么情况下yyerror才会执行,yychar相关的东西也一概不懂。   唉,还是再读bison手册吧,尽管之前都是秉持了“书不是卷轴”的原则,按章节跳读了很多内容,可是在通读第一章之后,我明白了通读还是很重要的。算一下时间,200多页的英文大概要1个月左右的时间吧,也不知道我能坚持多久,这一篇就暂时就此结尾吧。