|
|
|
|
@ -0,0 +1,122 @@
|
|
|
|
|
# Lab2:中间表示生成
|
|
|
|
|
|
|
|
|
|
## 1. Lab2 要求
|
|
|
|
|
|
|
|
|
|
Lab2要求可以将Sysy源程序转化为中间代码表示,经过Lab1理论上你目前的编译器已经可以将任意的Sysy程序转化为语法树,也就是Lab2的任务是完成遍历语法树生成中间代码的工作。
|
|
|
|
|
|
|
|
|
|
## 2. 相关文件
|
|
|
|
|
|
|
|
|
|
以下文件与本实验内容相关,可以优先看`./src/frontend/irgen.rs`,这个文件是整个IR生成的核心生成逻辑文件。
|
|
|
|
|
|
|
|
|
|
- 用到的数据结构
|
|
|
|
|
```bash
|
|
|
|
|
./src/utils/storage.rs
|
|
|
|
|
./src/utils/linked_list.rs
|
|
|
|
|
```
|
|
|
|
|
- 定义的IR数据结构
|
|
|
|
|
```bash
|
|
|
|
|
./src/frontend/ir/context.rs
|
|
|
|
|
./src/frontend/ir/function.rs
|
|
|
|
|
./src/frontend/ir/basicblock.rs
|
|
|
|
|
./src/frontend/ir/instruction.rs
|
|
|
|
|
./src/frontend/ir/global.rs
|
|
|
|
|
./src/typ.rs
|
|
|
|
|
./src/value.rs
|
|
|
|
|
./src/defuse.rs
|
|
|
|
|
```
|
|
|
|
|
- IR生成器
|
|
|
|
|
```bash
|
|
|
|
|
./src/typecheck.rs
|
|
|
|
|
./src/frontend/irgen.rs
|
|
|
|
|
./src/frontend/ir2string.rs
|
|
|
|
|
```
|
|
|
|
|
- 符号表
|
|
|
|
|
```bash
|
|
|
|
|
./src/frontend/symboltable.rs
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 3. 实现说明
|
|
|
|
|
|
|
|
|
|
### 3.1 关于`storage.rs`和`linked_list.rs`
|
|
|
|
|
|
|
|
|
|
> 这两个文件构成了一个**专门为编译器实现设计的基础数据结构库**,主要用于解决编译过程中抽象语法树(AST)和中间表示(IR)的存储、访问和遍历问题。**将存储管理和组织结构分离**,`storage.rs`解决的是“节点存放在哪里”的问题,通过内存池提供稳定的存储位置,`linked_list.rs`解决的是“节点之间如何组织”的问题,通过侵入式链表提供灵活的结构关系这种分离使得一个节点可以同时属于多个不同的组织结构,而无需为每种组织结构重复存储节点数据,这正符合编译器实现中处理复杂语法结构的需求。
|
|
|
|
|
1. `storage.rs` 是一个内存池分配器,这是一个**自定义的内存管理基础设施**,其核心思想是通过索引而非指针来引用数据。
|
|
|
|
|
- `GenericPtr<Data>`是一个**智能句柄**,本质上是对数组索引的封装。它不直接持有数据的引用,而是记录了数据在内存池中的位置。这样做的好处是句柄本身可以自由复制而无需考虑生命周期问题,且不受Rust借用检查器的约束。
|
|
|
|
|
- `GenericArena<Data>`是一个**连续存储的内存池**,内部使用`Vec`管理所有数据条目。它维护了一个空闲列表(`free_head`)来记录已被释放的位置,实现了O(1)的分配和释放操作。与普通`Vec`不同,Arena中数据的地址(即索引)一旦分配就保持稳定,不会因为后续的内存分配而改变。
|
|
|
|
|
- `UniqueArena<T>`是一个**值唯一的内存池**,在`GenericArena`的基础上增加了哈希索引。它确保相同的值在内存池中只存储一份,实现了类似字符串驻留的机制。这在编译器中用于标识符、字面量等需要唯一化存储的场景。
|
|
|
|
|
2. `linked_list.rs` 是一个侵入式链表,其特点是链接信息直接存储在节点内部,而非由容器管理。
|
|
|
|
|
- `LinkedListNode`特性定义了节点作为链表元素所需具备的行为,包括获取和设置前驱后继节点,以及节点所属容器的信息。任何需要被组织成链表的类型都必须实现这个trait。
|
|
|
|
|
- `LinkedListContainer` 特性定义了链表容器所需的行为,包括管理链表的头尾节点,以及在链表层面进行操作的接口。它提供了在链表前后添加节点、合并链表、分割链表等操作。
|
|
|
|
|
- `LinkedListIterator` 和 `LinkedListCursor`是两种遍历链表的方式:
|
|
|
|
|
- **迭代器**提供了标准的双端迭代能力
|
|
|
|
|
- **游标**提供了更灵活的遍历控制,支持不同的访问策略(先访问后移动或先移动后访问)和遍历方向
|
|
|
|
|
|
|
|
|
|
### 3.2 关于`defuse.rs`
|
|
|
|
|
|
|
|
|
|
> 这个文件实现了一个**定义-使用链(Def-Use Chain)**的数据结构,定义-使用链记录了程序中**值的定义**与其**使用位置**之间的关系。在编译器中间表示(IR)中,每条指令通常会定义一个新的值(如运算结果),同时也会使用一些已经定义的值(如操作数)。建立这些关系对于数据流分析、死代码消除、常量传播等优化都很重要。
|
|
|
|
|
1. `Useable` 特性是一个**标记接口**,表明一个类型可以作为“被使用的值”。任何实现了这个trait的类型(如指令、基本块参数等)都可以维护自己的使用者列表。
|
|
|
|
|
2. `User<T>`是一个**使用位置描述符**,记录了某个值在何处被使用。它由**使用者指令**(哪条指令使用了这个值)和**操作数索引**(在该指令的哪个操作数位置使用了这个值)组成。值得注意的是,`User<T>`是一个泛型结构,但其泛型参数`T`只存在于类型标记(`PhantomData`)中,并不实际存储T类型的值。这意味着同一个`User`结构可以适用于不同类型的被使用值,类型系统可以确保类型安全,但运行时没有额外开销
|
|
|
|
|
|
|
|
|
|
以一条加法指令为例:
|
|
|
|
|
```
|
|
|
|
|
%3 = add %1 %2
|
|
|
|
|
```
|
|
|
|
|
这条指令定义了一个新值`%3`,同时使用了两个已经存在的值`%1`和`%2`。在Def-Use链中:
|
|
|
|
|
- 对于`%1`的值,会记录一个使用者`User { instruction: %3所在指令, index: 1 }`
|
|
|
|
|
- 对于`%2`的值,会记录一个使用者`User { instruction: %3所在指令, index: 2 }`
|
|
|
|
|
- 对于`%3`的值,如果它在后续指令中被使用,也会记录相应的使用者
|
|
|
|
|
|
|
|
|
|
> **说明:** 对于一个最小化的Demo来说原本可以省略这个的实现,,但是还是加上了,是想说明中间代码的生成可以是很灵活的,在生成的过程中可以根据自己具体设计的数据结构加入很多自己的想法,比如在生成过程中直接维护好定义使用关系,不需要使用额外的遍来做这一步工作。整个中间代码的生成可以概括为遍历语法树创建IR,只要不影响这条主线的正确性,可以额外的自己添加维护一些感觉对后续机器代码生成或者优化实现有用的信息。
|
|
|
|
|
|
|
|
|
|
### 3.3 关于`irgen.rs`
|
|
|
|
|
|
|
|
|
|
> 这个文件实现了一个**从抽象语法树(AST)到中间表示(IR)的转换器**,是编译器前端与中端的核心接口。IR生成器负责将经过语法分析得到的抽象语法树转换为编译器内部使用的中间表示。这个过程本质上是一种**语义保留的结构变换**,将源语言的层次化结构转化为更适合分析与优化的线性化表示。
|
|
|
|
|
1. `IrGenResult`是一个**统一的结果类型**,用于区分两种不同的IR实体,这种区分是因为全局变量和局部值的访问方式不同。
|
|
|
|
|
- **全局变量**(`Global`):存储在静态数据区,需要通过地址访问
|
|
|
|
|
- **普通值**(`Value`):可以是常量、临时变量或指令结果
|
|
|
|
|
2. `IrGenContext`是**IR生成过程中的上下文环境**,维护了生成过程中需要的所有状态信息,这里的iv、v因为示例代码不支持函数调用和循环或者if指令,所以其实没有用到,给出是想更清晰的展示IrGenContext存在的意义。
|
|
|
|
|
1. **核心上下文**(`ctx`):整个IR模块的全局信息
|
|
|
|
|
2. **符号表**(`symboltable`):记录变量定义与使用的关系
|
|
|
|
|
3. **当前函数状态**(`cur_func`、`cur_func_id`、`cur_bbk`):跟踪正在生成的函数及其基本块
|
|
|
|
|
4. **循环控制栈**(`loop_entry_stack`、`loop_exit_stack`):处理循环嵌套时的跳转目标
|
|
|
|
|
5. **返回地址信息**(`cur_ret_slot`、`cur_ret_bbk`):统一函数出口的管理。
|
|
|
|
|
|
|
|
|
|
### 3.4 关于`ir2string.rs`
|
|
|
|
|
|
|
|
|
|
> 这个文件实现了一个**将IR转换为字符串的接口**,用于调试和测试。从这个文件我们可以得到一个认识是我们编译器内存中的IR和我们输出的IR其实是可以不一致的,我们编译器中的IR可以并不完全遵循LLVM IR的规范,但是只要输出的文本格式符合LLVM IR的.ll文件的语法要求就可以借助LLVM的工具。
|
|
|
|
|
|
|
|
|
|
## 4. 当前示例实现说明
|
|
|
|
|
|
|
|
|
|
同Lab1,本实例代码面向的时Sysy的简化子集,只支持int和void类型以及加法运算,扩展的巴克斯范式可以参见Lab1的相关文档,具体的一个示例可以参考`test/test_case/functional/simple_add.sy`,IR命令的支持也仅支持Sysy简化子集,仅包含如下指令:
|
|
|
|
|
| 类别 | 助记符 | 完整格式 | 操作数 | 返回值 |
|
|
|
|
|
|------|--------|----------|--------|--------|
|
|
|
|
|
| 终结 | `ret` | `ret void` | 0 | 无 |
|
|
|
|
|
| 终结 | `ret` | `ret <type> <value>` | 1 (值) | 无 |
|
|
|
|
|
| 终结 | `br` | `br label <dest>` | 1 (基本块) | 无 |
|
|
|
|
|
| 双目 | `add` | `<result> = add <type> <op1>, <op2>` | 2 (左值, 右值) | 运算结果 |
|
|
|
|
|
| 访存 | `alloca` | `<result> = alloca <type>` | 0 | 指针 |
|
|
|
|
|
| 访存 | `store` | `store <type> <value>, <type>* <pointer>` | 2 (值, 指针) | 无 |
|
|
|
|
|
| 访存 | `load` | `<result> = load <type>, <type>* <pointer>` | 1 (指针) | 加载的值 |
|
|
|
|
|
|
|
|
|
|
## 5. 构建与生成流程
|
|
|
|
|
|
|
|
|
|
运行实例代码,查看实验1的具体效果可以使用下面的命令,在项目根目录下:
|
|
|
|
|
```bash
|
|
|
|
|
cargo build -r
|
|
|
|
|
./target/release/compiler -ir ./test/output/simple_add.ll ./test/test_case/functional/simple_add.sy
|
|
|
|
|
```
|
|
|
|
|
这样解析出来的语法树会输出到`./test/output/simple_add.ll`文件中。
|
|
|
|
|
|
|
|
|
|
如果输出的IR符合LLVM IR的语法(实验示例代码所提供的IR生成器生成的IR符合LLVM IR语法),可以使用下面的命令运行.ll文件:
|
|
|
|
|
```bash
|
|
|
|
|
lli ./test/output/simple_add.ll
|
|
|
|
|
```
|
|
|
|
|
然后可以通过下面这条命令产看运行main函数的返回值,这条命令的本质是查看上一个运行程序的结束状态
|
|
|
|
|
```bash
|
|
|
|
|
echo $?
|
|
|
|
|
```
|
|
|
|
|
效果可以参见下图:
|
|
|
|
|

|
|
|
|
|
当然,我们更建议通过写Python或者.sh脚本的方式将运行测试流程自动化
|
|
|
|
|
|
|
|
|
|
|