|
|
# PW6 实验报告
|
|
|
|
|
|
学号PB21000179 姓名 吴书让
|
|
|
学号PB21111627 姓名 罗胤玻
|
|
|
|
|
|
## 问题回答
|
|
|
|
|
|
### Task1
|
|
|
|
|
|
**手工代码不具代表性,以下分析针对使用** `clang -S -emit-llvm` **编译得到的IR代码**
|
|
|
|
|
|
> 1-1 请给出while语句对应的LLVM IR的代码布局特点,重点解释其中涉及的几个`br`指令的含义(包含各个参数的含义)
|
|
|
|
|
|
**1-1:**
|
|
|
生成代码并对主要部分加入注释如下:
|
|
|
```llvm
|
|
|
@b = dso_local global i32 0, align 4 ; 定义一个全局变量b,初始值为0,对齐方式为4
|
|
|
@a = dso_local global i32 0, align 4 ; 定义一个全局变量a,初始值为0,对齐方式为4
|
|
|
|
|
|
; Function Attrs: noinline nounwind optnone uwtable
|
|
|
define dso_local i32 @main() #0 { ; 定义一个名为main的函数,返回类型为i32
|
|
|
%1 = alloca i32, align 4 ; 在栈上分配一个i32类型的空间,对齐方式为4
|
|
|
store i32 0, i32* %1, align 4 ; 将0存储到%1指向的地址,对齐方式为4
|
|
|
store i32 0, i32* @b, align 4 ; 将0存储到全局变量b,对齐方式为4
|
|
|
store i32 3, i32* @a, align 4 ; 将3存储到全局变量a,对齐方式为4
|
|
|
br label %2 ; 无条件跳转到标签2
|
|
|
|
|
|
2: ; 标签2(循环条件判断基本块)
|
|
|
%3 = load i32, i32* @a, align 4 ; 从全局变量a中加载一个i32类型的值,对齐方式为4
|
|
|
%4 = icmp sgt i32 %3, 0 ; 比较%3和0的大小关系,结果存储在%4中
|
|
|
br i1 %4, label %5, label %11 ; 根据%4的值进行条件跳转,如果为真跳转到标签5,否则跳转到标签11
|
|
|
|
|
|
5: ; 标签5(循环体基本块)
|
|
|
%6 = load i32, i32* @b, align 4 ; 从全局变量b中加载一个i32类型的值,对齐方式为4
|
|
|
%7 = load i32, i32* @a, align 4 ; 从全局变量a中加载一个i32类型的值,对齐方式为4
|
|
|
%8 = add nsw i32 %6, %7 ; 将%6和%7相加,结果存储在%8中
|
|
|
store i32 %8, i32* @b, align 4 ; 将%8存储到全局变量b,对齐方式为4
|
|
|
%9 = load i32, i32* @a, align 4 ; 从全局变量a中加载一个i32类型的值,对齐方式为4
|
|
|
%10 = sub nsw i32 %9, 1 ; 将%9减1,结果存储在%10中
|
|
|
store i32 %10, i32* @a, align 4 ; 将%10存储到全局变量a,对齐方式为4
|
|
|
br label %2 ; 无条件跳转到标签2
|
|
|
|
|
|
11: ; 标签11(循环结束后基本块)
|
|
|
%12 = load i32, i32* @b, align 4 ; 从全局变量b中加载一个i32类型的值,对齐方式为4
|
|
|
ret i32 %12 ; 返回%12作为函数的返回值
|
|
|
}
|
|
|
```
|
|
|
|
|
|
注意到如下的while语句对应的LLVM IR的代码布局特点:
|
|
|
1. while语句的条件判断部分和循环体部分分别对应一个基本块,
|
|
|
2. 循环体部分的最后一条语句为无条件跳转到条件判断部分的基本块,
|
|
|
3. 条件判断部分的最后一条语句为条件跳转到循环体部分的基本块或者跳转到循环体部分之后的基本块。
|
|
|
|
|
|
|
|
|
> 1-2 请简述函数调用语句对应的LLVM IR的代码特点
|
|
|
|
|
|
**1-2:**
|
|
|
生成代码并对主要部分加入注释如下:
|
|
|
```llvm
|
|
|
define dso_local i32 @add(i32 %0, i32 %1) #0 {
|
|
|
%3 = alloca i32, align 4 ; 在栈上分配一个i32类型的空间,对齐方式为4
|
|
|
%4 = alloca i32, align 4 ; 在栈上分配另一个i32类型的空间,对齐方式为4
|
|
|
store i32 %0, i32* %3, align 4 ; 将参数%0的值存储到%3指向的地址中,对齐方式为4
|
|
|
store i32 %1, i32* %4, align 4 ; 将参数%1的值存储到%4指向的地址中,对齐方式为4
|
|
|
%5 = load i32, i32* %3, align 4 ; 从%3指向的地址中加载一个i32类型的值,对齐方式为4
|
|
|
%6 = load i32, i32* %4, align 4 ; 从%4指向的地址中加载一个i32类型的值,对齐方式为4
|
|
|
%7 = add nsw i32 %5, %6 ; 将%5和%6相加,结果存储在%7中
|
|
|
%8 = sub nsw i32 %7, 1 ; 将%7减1,结果存储在%8中
|
|
|
ret i32 %8 ; 返回%8
|
|
|
}
|
|
|
|
|
|
define dso_local i32 @main() #0 { ; 定义一个名为main的函数,返回类型为i32
|
|
|
%1 = alloca i32, align 4 ; 在栈上分配一个i32类型的空间,对齐方式为4
|
|
|
store i32 3, i32* %1, align 4 ; 将值3存储到%1指向的地址中,对齐方式为4
|
|
|
%2 = alloca i32, align 4 ; 在栈上分配另一个i32类型的空间,对齐方式为4
|
|
|
store i32 2, i32* %2, align 4 ; 将值2存储到%2指向的地址中,对齐方式为4
|
|
|
%3 = load i32, i32* %1, align 4 ; 从%1指向的地址中加载一个i32类型的值,对齐方式为4
|
|
|
%4 = load i32, i32* %2, align 4 ; 从%2指向的地址中加载一个i32类型的值,对齐方式为4
|
|
|
%5 = call i32 @add(i32 %3, i32 %4) ; 调用add函数,传入参数%3和%4,并将返回值存储到%5中
|
|
|
%6 = add nsw i32 %5, 1 ; 将%5加1,结果存储在%6中
|
|
|
ret i32 %6 ; 从函数中返回%6的值
|
|
|
}
|
|
|
```
|
|
|
|
|
|
注意到如下的函数调用语句对应的LLVM IR的代码特点:
|
|
|
1. 函数调用语句对应的LLVM IR的代码中,会先将函数的参数存储到栈上分配的空间中,然后再调用函数。
|
|
|
2. 函数调用语句对应的LLVM IR的代码中,会将函数的返回值存储到一个临时变量中,然后再对临时变量进行操作。
|
|
|
3. 调用者使用`call`指令调用被调用者,被调用者使用`ret`指令返回调用者。
|
|
|
|
|
|
------
|
|
|
|
|
|
### Task2
|
|
|
|
|
|
> 2-1 请给出`SysYFIR.md`中提到的两种getelementptr用法的区别, 并解释原因:
|
|
|
|
|
|
**2-1:**
|
|
|
- `getelementptr`实际上是一条指针计算语句,不进行任何数据的访问或修改,作用是计算指针并修改计算后指针的类型。
|
|
|
- 第一个参数为要进行计算原始指针的类型;
|
|
|
- 第二个参数是原始指针,往往是一个结构体指针,或数组首地址指针。
|
|
|
- 第二个参数及以后的参数,都称为index,表示要进行计算的参数,作用在第二个参数给出的初始指针,如结构体的第几个元素,数组的第几个元素。
|
|
|
- **第一种用法**:获取数组元素所在地址,一个偏移0得到%1指向数组的第一个,用第二个偏移0得到首元素向后偏移量,返回此地址
|
|
|
- **第二种用法**:获取指针地址,第一个偏移0计算得到%1所指的变量,并返回该变量的地址
|
|
|
|
|
|
-----
|
|
|
|
|
|
### Task3
|
|
|
|
|
|
> 3-1. 在`scope`内单独处理`func`的好处有哪些。
|
|
|
|
|
|
**3-1:** 在不同的scope中可以使用相同的变量名,而不必担心命名冲突的问题,同时也可以提高代码的可读性和可维护性。
|
|
|
|
|
|
|
|
|
## 实验设计
|
|
|
本次实验循序渐进,分三步指导完成一个sy语言的机器语言自动生成器。
|
|
|
1. 手写机器语言,熟悉机器语言结构及各类语句含义和用途
|
|
|
2. 手动使用接口设置机器语言,相比第一步抽象层次更高,熟悉各种接口含义以及使用方法
|
|
|
3. 结合PW5中学习的AST以及访问者模式,为sy语言自动翻译机器语言。前两步中的四个例子几乎已经涵盖所有情况,可以作为参考。
|
|
|
|
|
|
## 实验难点及解决方案
|
|
|
0. educoder设计麻烦
|
|
|
1. 多维数组处理麻烦:因为一开始陷入了想用多维数组来存储的思维,导致很多不必要的尝试;换成用一维数组实现就很简单。
|
|
|
2. 函数全为void型,不能返回变量:设置全局临时变量作为返回值,定时整理删减全局临时变量。
|
|
|
3. 一开始随意选择了某个语法书节点开始编程,但是前置节点尚未编写导致了理解上的困难:审视全体语法树节点,尽可能选取最上层节点或着依赖较少的节点开始编写,比如funcDef。
|
|
|
4. 测试目录中只有20个文件,但是test.py提示进行了21次测试且仅有第一次测试出错:删去.DS_Store并加入.gitignore。
|
|
|
5. 重复表达式过多,修改麻烦:定期整理重复代码抽象出来作为单独函数。
|
|
|
|
|
|
## 实验总结
|
|
|
挺好的一次实验,但是感觉第二步到第三步之间缺少指引,比如翻译器的整体规划(一般涉及到一些全局变量,转换等)。这也导致了,如果没有事先商量仅仅指派某些函数来分工的话,难以同步进度和各种操作。
|
|
|
|
|
|
## 实验反馈
|
|
|
1. 建议完全抛弃educoder。
|
|
|
2. 可以在2、3步之间加入一个阶段,让学生分列出各语法树节点的可能情况以及处理方案,方便整体审视检查。之后只需要对着事先计划好的处理方案编程即可, 相比直接编写难度会小很多。
|
|
|
|
|
|
## 组间交流
|
|
|
提前商定好各函数之间必要信息交换,以及常用辅助函数和全局变量的设计。
|
|
|
|
|
|
然后各自负责一部分visit的编写,完成后进行整体调试和debug。
|
|
|
|
|
|
在Pass所有20个test后,针对各个test的情况进行讨论(见下),对未覆盖的情况增加test(新增的test检查出了BB命名重复的错误)。
|
|
|
```
|
|
|
1. main函数
|
|
|
2. [pass] 变量定义,未检查float类型->21 -
|
|
|
3. 空语句
|
|
|
4. [pass] 一维数组类型定义,未检查float类型与赋初值情况->22
|
|
|
5. [pass] 一维数组赋值情况->22
|
|
|
6. [pass] 常量定义,未检查非全局变量情况->23
|
|
|
7. [pass] 常量一维数组定义,未检查float类型->24
|
|
|
8. 函数定义,未检查函数含参数情况
|
|
|
9. [pass] 函数定义,未检查函数参数为float类型情况->25
|
|
|
10. if语句
|
|
|
11. [pass] if-else语句,未检查if嵌套情况-> 26
|
|
|
12. while语句,未检查while嵌套-> 27,未看出错误原因
|
|
|
13. break语句
|
|
|
14. continue语句
|
|
|
15. 输入语句 getint
|
|
|
16. 复杂函数定义
|
|
|
17. 等于关系测试,未检查其他关系-> 28,多个if语句块之间疑似未按照原有顺序编译
|
|
|
18. scop作用检查,变量作用域问题
|
|
|
19. 最大公因数
|
|
|
20. 汉诺塔问题,递归功能检测
|
|
|
```
|
|
|
|
|
|
完成主要任务后开始编写多维数组,首先讨论方案,决定用一维数组代替,其次编写了多种test辅助验证正确性。
|
|
|
|
|
|
在完成的过程中,时刻进行测试保证之前的test依然Pass。
|
|
|
|
|
|
组内主要使用SNS及时沟通,使用git同步进度。 |