diff --git a/CS143体验报告.md b/CS143体验报告.md index 78664ad..491b51f 100644 --- a/CS143体验报告.md +++ b/CS143体验报告.md @@ -278,4 +278,49 @@ perl pa5-grading.pl -r #### 实现说明 -这个里面的细节实在是太多了,如果要写清楚 \ No newline at end of file +这个里面的细节实在是太多了,如果要全部写清楚的话就很抽象,所以就简单写写要点吧。 + +在开始之前,建议把它实现的继承树实现给换成 PA4 里面自己写的,这样可以省去看它的抽象代码的功夫。平心而论,他写的基础代码质量真的不咋地,效率低下还有内存泄漏,无力吐槽。 + +主要的实现还是分成两个部分,第一个部分先线性地去生成一些必要的静态数据表,然后再递归地去生成表达式代码。这也说明了整个代码的结构,前面一堆是数据段,后面一堆是代码段。 + +关于静态数据的生成,主要参考 Runtime System 说明,里面应该把所有必须生成和建议的实现方式都说到了。然后 skeleton 已经写了一些部分的生成代码,主要看 `CgenClassTable::code()`,它生成了所有需要用 `.globl` 声明的符号(但是可能没有定义,具体的符号见 Figure 3)、所有的常量对象(String、Int、Bool 这三种基本类型的字面值常量,都被转换成了静态的对象格式存放在数据段里面)以及 GC 标志。我们需要生成的东西有: +1. `class_nameTab`:类名表,按照 class tag 排序 +2. `_dispTab`:每个对象的函数表,可以自己设计,不过还是直接按照继承和扫描顺序填算了 +3. `_protoObj`:每个对象的原型对象,建议给每个属性填上默认值,这样后面就可以少生成一些代码,因为复制的时候就相当于已经把没有初始化表达式的属性给赋默认值了,不需要单独生成加载默认值的代码了。 +4. `class_objTab`:这个在 `cool-tour.pdf` 里面没写,但是单独的 `cool-runtime.pdf` 里面写了。它的用处是处理 `SELF_TYPE` 的东西。如果对象是 `SELF_TYPE`,就只能通过 class tag 来确定它的运行时类型然后动态绑定,而不能在编译时确定它调用的方法。所以需要一个对象表,里面放上 `_init` 和 `_protoObj`,用的时候拿 tag 查表。 + +为了生成这些表,还需要做一些额外的工作,不过最后思考一下,可以在一次先序遍历中解决所有的问题。为了生成 dispatch 表,需要统计一个类中的所有方法,这就涉及到了继承和重写基类方法;为了生成 protoObj,需要指派 tag 并统计所有的属性,这里也需要考虑继承的问题。因为后面要用到 tag 在运行时判断继承关系,因此 tag 不能随便分配,需要按照遍历顺序依次分配。 + +做完以上的工作之后,我们就可以开始进行正式的代码生成了。代码生成主要是表达式的生成,最后每个函数包装一下。我基本上就是按照课上讲的 Accumulator 模型写的,除非是调用 Runtime 的地方,其他的代码都仅使用 `a0` 和 `t1` 这两个寄存器做计算。不过课上的模型毕竟是简化过的,基本上能生成一大半的代码。剩下的部分需要额外处理局部变量的引入和属性的查询,需要自己写一个从符号到存储位置的映射表。因为我懒得单独做临时变量的地址分配了(其实不难做,只需要额外遍历一次 AST 就可以了),所以分配临时变量就直接压栈了。不过索引临时变量还是用的是 `$fp` 的相对偏移,因此维护了一个全局变量来记录当前用了分配的变量用的是哪一个地址(类似于一个小的栈,分配的时候+1,离开作用域的时候-1)。函数的参数也在栈上,在函数开始时候要把参数加入到表里。当然,除了栈之外,属性也是映射表的一部分,所以表项要加一个域来指示符号是在栈上还是在堆上(即相对于对象指针的偏移量)。 + +除了表达式的生成,还有函数的生成。 + +首先需要确定的就是 calling convention,我是用了比较方便实现的结构:调用者只需要压参数就行了,然后把被调用者所属的对象指针塞进 `$a0`;被调用者负责保存 `$fp,$s0,$ra` 这三个寄存器,同时负责回收参数占用的空间。 + +关于参数,由于 COOL 规定了函数参数从左到右求值(冷知识:C 语言没有规定,取决于编译器实现),因此我让栈顶放最后一个参数(但是正常的系统中是栈顶是第一个参数)这样比较好写(求值一个 push 一个),因此在取参数的时候要特别注意。 + +关于寄存器的使用,一共涉及到 8 个寄存器 +1. `$sp` 栈指针,动态变化,需要维护栈平衡 +2. `$fp` 帧指针,在一个函数上下文中是固定的,用来确定局部变量的位置 +3. `$ra` 返回地址,没啥好说的。 +4. `$s0` 存放 self object,每个函数可能不一样,所以要在入口保存旧的并加载新的,在出口恢复旧的。 +5. `$a0` 有两种情况,一种是作为参数,一般是把目标函数的 self object 放在里面传进过程调用里面;另一种是作为返回值,无论是函数的返回值还是表达式的返回值,反正得到的结果都放在里面。 +6. `$t1` 临时变量,因为 RISC 架构只能两个寄存器之间做运算。 +7. `$a1` `$t2` 在我的实现中,这两个仅仅是用来给 Runtime 传参数的时候才会设置的,其他地方不会用到也不需要保存。 + +需要生成的代码有两大块,对象初始化代码和方法代码。 +新建一个对象分两步,首先调用 `Object.copy` 从原型对象复制一份,然后调用对应类的初始化方法来完成属性初始化。初始化方法只有一个参数 `$a0` 放的是需要被初始化的对象指针,主要是也是两步,先调用基类的初始化代码,然后把自己新增的且有初始化表达式的属性依次按表达式求值。之所以只生成有 init 表达式的属性代码,是因为前面写原型的时候就把那些没有 init 表达式的属性的默认值给填上去了。 +方法代码就正常生成吧,不同的是,初始化代码每个类都要生成,而方法代码则不需要生成预定义的5个类的方法。还有就是生成之前记得把参数列表和属性给加到符号表里面。 + +关于实现部分,就简单描述如上,剩下的细节就去看手册吧,写不动。 + +### 完结感言 + +至此,这个编译器就算是做完了。实验是 3.17 正式开始做的,前天调完最后一个 PA 是 4.1,前前后后做了有半个月的样子,一共写了近 4000 行代码。唯一的感觉是,真累啊。这个 Lab 既没有友好的框架代码,也没有完善的文档说明,工程量还巨大,如果不是我特地去找了找测试脚本,估计还得再多花不少时间写测试样例。总之相比做过的其他几个实验,这个的体验顶多算是及格。 + +就难度而言,其实是没有多难的,看上去折磨只不过是因为它的不完善导致每个 PA 开始之前会困惑好长时间,然后堆代码堆个上千行,最后调 edge case 再调半天。那些算法一个没做,我觉得倒是不如像 xv6 那样,给一个基础的系统,然后让我写寄存器分配或者是控制流分析这些进阶功能。对于编译的理解,好像也没有在理论课的基础上提升多少:PA1 学习 COOL,没啥大用;PA2 写 flex,练了练怎么写正则表达式;PA3 写 Bison,练了练怎么写 BNF 文法,背后的原理啥也没学会;PA4 写语义检查,也就是实现了课上讲的类型规则系统;PA5 生成代码,纯纯的 MIPS 汇编。 + +硬要说我学到了什么的话,大概就是了解了一下编译器的完整流程吧。总之不是很推荐做这个实验,有这功夫不如去写写它的 Written Assignment,还有详细的参考答案,感觉比做这个 PA 要更有用那么一些。 + +无论如何,完结撒花,就酱。 \ No newline at end of file