# lab3 实验报告 > PB20111654 李晓奇 ## 实验要求 用访问者模式来实现 IR 的自动生成。 ## 实验难点 做实验时我把令我**费解的部分**都记录了下来,经过整理后如下: 1. syntax_tree和ast的区别、为什需要ast? 阅读AST的构造函数,`AST(syntax_tree *)` 实际上这部分在上个实验就有看calc_ast的代码,但是印象不深刻。 2. 访问者模式究竟有什么用? 比如这样一系列函数调用: ```cpp ASTPrinter::visit(ASTProgram &) { //... for (auto decl : node.declarations) { decl->accept(*this); } //... } ASTVarDeclaration::accept(ASTVisitor &) { //... auto var_decl = dynamic_cast(this); if (var_decl) { var_decl->accept(visitor); return; } //... } void ASTVarDeclaration::accept(ASTVisitor &visitor) { visitor.visit(*this); } ``` 假设就是vardeclaration,直接调用(在`ASTPrinter::visit`内)`visit(dynamic_cast(this))`不是一样的吗? 参考[攻略](https://www.jianshu.com/p/1f1049d0a0f4),我对这种设计模式 理解/接受 了一些。也在这里意识到要做的事:实现一个访问者类,和`ASTPrinter`类似。 3. 无从下手 每个节点的访问都要做什么?比如我访问`ASTFunDeclaration`时,我要为参数做什么?我有什么? 答案:从变量开始! 自底向上做自然多了,不然像在摸黑拧魔方。 4. 如何获得返回值? 加入一个全局变量保存语句的返回值。 5. 数组or指针? 这个关乎地址的计算、类型转换…… 经过对比IR代码、反复测试,得出结论:数组类型只在变量声明中得到,函数传参一定是传一个指针过去。 记录**遇到的坑**: > 这次实验,配合算法的最后一个OJ,让我深刻认识到自己写bug的能力有多强。 - 形如这种产生式子`term→term mulop factor ∣ factor`,我为了图方便先求了`factor`,再根据`term`的有无调用`accept()`,但是这样会导致左右操作数颠倒。 - 我对于类型转换的控制比较严格,没考虑到的一率abort,因此很快发现自己漏掉了bool类型的转换。 - 接上条,一些类型转换的bug:`i32`到`i1`也是需要转换的,开始做的时候还没意识到这些(主要是没意识到`i1`的存在)。 - 函数传参 这个卡了我一阵子,经过反复的debug和思索,更见坚信:函数传参只是值传递。(至少在cminus是的),所有出现的bug也好、逻辑上的死循环也好,都应该从ASTVar的访问寻找病因。 - 全局变量要特殊对待。 - 数组下标为负时要调用`neg_idx_except()`,这个函数的call后边也要加上br。 - 神奇,bool变量不能比较,所以在比较、计算的时候要做类型转换。 - store的顺序颠倒了!! - GEP和Load的翻译有问题。 定位问题:源操作数不是array类型! 那一定要看声明时push进scope的是什么,发现声明的数组类型其实也是指针类型(指向数组的指针),并不是数组?? 但后来发现,原来问题出在调用GEP,我直接把0代替`CONST_INT(0)`传入了,“胡闹吗这不是”。 - 不要乱给lable命名,因为lable的命名不会自动为自定义名称的label去重。 - 发现ifelse语句忘了无条件跳转了…… - 返现布尔表达式必须使用i1类型,类型要求真的太严格了!! - 知道了为什么clang生成的中间代码,会把实参声明一遍再拷贝了。 在我看来方便编译器代码的生成,因为重新声明,push给scope的值是指针类型而不是纯值,这在后边访问Var中有用。 - 发现ret会有乱跳转的问题,这个在最后一个测试的abs中发现。 问题出在:我为if_else语句设置了三个测试块,分别负责if,else,和跳出。但是有一处的块是空的,就会导致生成默认返回。 后来发这里其实并没有问题,如果有返回值,在if或者else内部就返回了,并不会跳出去被默认返回扰乱。 - 最后一个测试文件能够正常执行,但是输出有错。 > 这个测试文件的调试吃了我三个多小时的时间。 尝试了: - 和clang生成的替换(只能替换整个函数),定位问题出在函数`gauss`上。 - 不断缩减guass函数的语句范围,测试输出是否与clang编译出的输出一致。但是最后缩减到的语句还是很复杂,IR还是有200+行。 - 寻找可以单步调试ll文件的调试器,考虑到没用,放弃。 - 利用opt、dot生成基本块的跳转图,还是大的离谱。 - 助教推荐的creduce,最开始以为是助教没理解我的问题,觉得永不上,舍弃,现在回想巨亏。 最后,找同学看一下IR代码,二十分钟就找到问题了,是把float变量声明成int了。 ## 实验设计 ### 1)全局变量的设计 有两个全局变量:`Value* cur_value`和`bool LV` 分别表示最近一个表达式返回值是多少、是否需要左值(赋值时使用)。 ### 2)难点解决 ASTVar的访问 最困难的函数当属`CminusfBuilder::visit(ASTVar &node)`,这个函数需要完成的主要任务是产生访问的局部变量的数值 or 地址。 根据是否需要左值、是否有下标索引、Var的类型,IR产生的逻辑各不相同。 我的逻辑如下: 首先断言访问到的节点如果合法,在scope中找到的一定是一个指针类型,指向的是基本类型(int|float|array)或者指针类型,指针类型特指函数声明中保存实参数值的情况,且参数类型为指针。所有push操作都应该维护这个性质。 其次,根据是否需要左值区分不同的逻辑。 - 根据我的设计,左值只有赋值语句出现,由于array和指针都不可以赋值,所以合法情况下,返回int|float的地址。 - 如果不是左值,根据var对应的类型区分: - int|float:返回对应的数值。 - array:返回首地址或者索引后的元素的地址,即可能传递指针也可能是传递内部某一个元素的值。 - 指针:返回指针或者索引后的元素地址。 以上对于int|float类型的访问,如果存在索引都是非法。 这里的难点主要是: 1. 确定var的可能类型:关于指针类型(名义上)我踩了很多坑才意识到。 2. 合理组织代码:写了不少bug才该正确。 3. 调用好API判断类型。 ### 3)IR冗余 只是探讨一下可能的方向。 - `neg_idx_except`的调用,如果一个函数中有很多数组索引,那么每次都建立一个基本块跳转到这个函数就显得比较冗余,考虑一个函数维护至多一个基本块来调用`neg_idx_except`。 - 或许也可以不为函数实参声明额外空间做保存。这里我还是不理解alloca在什么时候起作用(除了搞出一堆指针来折磨我之外),但是不为实参alloca新空间拷贝一下,IR代码至少是能跑的。我这么写是为了代码好写,或许可以改进。 ### 实验总结 > 充满哲理性的总结: > > 不管多困难,先开始。~~因为更多的困难在动手前是永远想不到的~~。 收获: - 最后和助教交流debug方法,发现了新工具。 - 对中间代码有了深刻的认识。 - 对中间代码的生成有了亲身体验。 ### 实验反馈 (可选 不计入评分) 加入调试的指导会让大家学到更多,实验更顺利。