Skip to content

Commit

Permalink
[docs] refactor docs directory
Browse files Browse the repository at this point in the history
  • Loading branch information
floatshadow committed Mar 3, 2024
1 parent 279f4a6 commit 80c8975
Show file tree
Hide file tree
Showing 10 changed files with 145 additions and 138 deletions.
8 changes: 0 additions & 8 deletions docs/ack.md

This file was deleted.

2 changes: 1 addition & 1 deletion docs/accipit-spec.md → docs/appendix/accipit-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ terminator ::= <jmp> | <br> | <ret>
- value binding,定义一个变量 `<symbol>`,并给它赋值,即将右侧指令的值赋给左侧 `<symbol>` 代表的变量。
- terminator,不进行任何赋值,标记基本块的终结,对应控制流的跳转。

**注意**:出于某种神秘的原因,我们规定每个变量只能在定义的时候被赋值一次,也就是说,每条指令左侧的变量 `<symbol>` 在对应的作用域内要求是**唯一**的,至于为什么,你可以参考 [附录](appendix.md)
**注意**:出于某种神秘的原因,我们规定每个变量只能在定义的时候被赋值一次,也就是说,每条指令左侧的变量 `<symbol>` 在对应的作用域内要求是**唯一**的,至于为什么,你可以参考 [附录](quads2ssa.md)
所以,我们在语法上用 `let` 来暗示这一点。


Expand Down
3 changes: 3 additions & 0 deletions docs/appendix/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# 附录

本条目包含一些阅读材料、前端语言和 IR 规范等辅助性内容
108 changes: 108 additions & 0 deletions docs/appendix/quads2ssa.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# 从四元数到静态单赋值形式

**提示**:你可以跳过阅读这一部分,但是阅读该部分可能有助于你理解本实验中间代码的设计.

常见的平台无关中间代码是线性的一串指令.
在早期,指令的设计风格通常是四元组 (quads) 形式,例如 `x = y binop z`.
其中有操作码 `binop`,两个源变量 `y``z`,以及一个目标变量 `x`,因此被称为“四元组”.
一种常见的实现方式如下:

```cpp
class Instruction {
// all possible opcode.
enum Opcode { ... };
Opcode op;
// id of destination variable.
int dst;
// id of first and second source variable.
int src0, src1;

// instructions connected as a linked list.
Instruction *next;
}
```
四元数看似很简单,但是有一个比较严重的问题,就是不太方便做代码优化,请看下面这条例子:
```plaintext
y = a add 1
x = y sub b
y = x add b
...
result = x add y
```

代码的优化经常需要追踪数据流,也就是追踪四元组中两个源变量的值是由哪条指令进行的赋值,又被哪些指令使用.
我们一步一步看.
首先是 `y = a add 1` 这条指令,似乎很显然,不是吗?
源变量 `a` 和常数 `1`.

但是遇到 `x = y sub b` 时,我们很快遇到了麻烦: 这条指令需要的 `y` 的值是哪里被赋值的,或者说 `y` 最新的值在哪里,是 `y = a add 1` 还是 `y = x add b`

![dfg01](../images/dfg01.svg)

接下来的 `y = x add b`,源变量 `b` 还是上一条指令 `x = y sub b` 中用到的那个 `b` 吗?还是有其他指令为 b 赋了新值?

![dfg02](../images/dfg02.svg)

某一条和它们隔得很远的指令 `result = x add y`,它们的 `x``y` 又从哪里来?

![dfg03](../images/dfg03.svg)

你会发现,我们一直需要知道某个源变量最新的赋值发生在哪里,这意味这:

- 要么每次从后往前扫描,第一个遇到的对源变量的赋值,就是最新的值,这样时间开销很大.
- 要么维护一个稠密的集合,记录当前指令前所有变量最新的赋值发生在哪里,这样在变量很多的情况下空间开销很大.

上述这种关系被称为 use-def chain,静态单赋值形式 (SSA) 的优点之一就在于它能较好地维护 use-def chain.SSA 的一个特点是每个变量仅赋值一次,因此,上面的代码需要写成:

```plaintext
y.0 = a.0 add 1
x.0 = y.0 sub b.0
y.1 = x.0 add b.0
...
result.1 = x.0 add y.1
```

- 由于只赋值一次,每个赋值的变量名都是独一无二的,因此你可以把赋值看作“定义” (define) 了一个新变量,这样我们就能明确地知道源操作数的值是怎么产生的.
- 更进一步地,由于变量只赋值一次,我们不需要记录原变量的 id 或者 name,源变量的使用 (use) 只需要一个指向定义这个变量的指令的指针就可以表示.
- 再进一步,既然原变量使用指针索引,那么指令里面目标变量也失去了意义,指令本身就可以指代“变量”本身.

经过这么一番改造,指令的实现大致如下所示:

```cpp
class Instruction {
// all possible opcode.
enum Opcode { ... };
Opcode op;

// first and second source variable.
// `use` of the `definition` of src0 and src1.
Instruction *src0, *src1;

// instructions connected as a linked list.
Instruction *next;
}
```
因此我们可以发现,SSA 风格的指令中,指令**使用** (use) 的操作数 `src0` 和 `src1` 直接指向了变量的**定义** (definition) 处,因此指令之间就像一个图一样标记了数据的流动。
使用 SSA 风格相比四元组风格具有以下优点:
- 减少“复制”操作,例如这是四元组风格:
```
t1 = #1
t2 = #2
t3 = t1 + t2
res = t3
```
SSA 语境下,由于指令就是指本身,指向指令 `t1 = #1` 的指针实际上就是指向常数值 `#1`,因此复制操作是冗余的,上面这段代码可表示为:
```
res = #1 + #2
```


如果你有兴趣,可以阅读 Cliff Click 的 [From Quads to Graphs](http://softlib.rice.edu/pub/CRPC-TRs/reports/CRPC-TR93366-S.pdf),以及 [SSA Book](https://link.springer.com/book/10.1007/978-3-030-80515-9)


126 changes: 8 additions & 118 deletions docs/appendix.md → docs/appendix/sysy-accipit-mapping.md
Original file line number Diff line number Diff line change
@@ -1,120 +1,10 @@
# 附录
# SysY 结构与 Accipit IR 的对应

## 从四元数到静态单赋值形式

**提示**:你可以跳过阅读这一部分,但是阅读该部分可能有助于你理解本实验中间代码的设计.

常见的平台无关中间代码是线性的一串指令.
在早期,指令的设计风格通常是四元组 (quads) 形式,例如 `x = y binop z`.
其中有操作码 `binop`,两个源变量 `y``z`,以及一个目标变量 `x`,因此被称为“四元组”.
一种常见的实现方式如下:

```cpp
class Instruction {
// all possible opcode.
enum Opcode { ... };
Opcode op;
// id of destination variable.
int dst;
// id of first and second source variable.
int src0, src1;

// instructions connected as a linked list.
Instruction *next;
}
```
四元数看似很简单,但是有一个比较严重的问题,就是不太方便做代码优化,请看下面这条例子:
```plaintext
y = a add 1
x = y sub b
y = x add b
...
result = x add y
```

代码的优化经常需要追踪数据流,也就是追踪四元组中两个源变量的值是由哪条指令进行的赋值,又被哪些指令使用.
我们一步一步看.
首先是 `y = a add 1` 这条指令,似乎很显然,不是吗?
源变量 `a` 和常数 `1`.

但是遇到 `x = y sub b` 时,我们很快遇到了麻烦: 这条指令需要的 `y` 的值是哪里被赋值的,或者说 `y` 最新的值在哪里,是 `y = a add 1` 还是 `y = x add b`

![dfg01](images/dfg01.svg)

接下来的 `y = x add b`,源变量 `b` 还是上一条指令 `x = y sub b` 中用到的那个 `b` 吗?还是有其他指令为 b 赋了新值?

![dfg](images/dfg02.svg)

某一条和它们隔得很远的指令 `result = x add y`,它们的 `x``y` 又从哪里来?

![dfg](images/dfg03.svg)

你会发现,我们一直需要知道某个源变量最新的赋值发生在哪里,这意味这:

- 要么每次从后往前扫描,第一个遇到的对源变量的赋值,就是最新的值,这样时间开销很大.
- 要么维护一个稠密的集合,记录当前指令前所有变量最新的赋值发生在哪里,这样在变量很多的情况下空间开销很大.

上述这种关系被称为 use-def chain,静态单赋值形式 (SSA) 的优点之一就在于它能较好地维护 use-def chain.SSA 的一个特点是每个变量仅赋值一次,因此,上面的代码需要写成:

```plaintext
y.0 = a.0 add 1
x.0 = y.0 sub b.0
y.1 = x.0 add b.0
...
result.1 = x.0 add y.1
```

- 由于只赋值一次,每个赋值的变量名都是独一无二的,因此你可以把赋值看作“定义” (define) 了一个新变量,这样我们就能明确地知道源操作数的值是怎么产生的.
- 更进一步地,由于变量只赋值一次,我们不需要记录原变量的 id 或者 name,源变量的使用 (use) 只需要一个指向定义这个变量的指令的指针就可以表示.
- 再进一步,既然原变量使用指针索引,那么指令里面目标变量也失去了意义,指令本身就可以指代“变量”本身.

经过这么一番改造,指令的实现大致如下所示:

```cpp
class Instruction {
// all possible opcode.
enum Opcode { ... };
Opcode op;

// first and second source variable.
// `use` of the `definition` of src0 and src1.
Instruction *src0, *src1;

// instructions connected as a linked list.
Instruction *next;
}
```
因此我们可以发现,SSA 风格的指令中,指令**使用** (use) 的操作数 `src0` 和 `src1` 直接指向了变量的**定义** (definition) 处,因此指令之间就像一个图一样标记了数据的流动。
使用 SSA 风格相比四元组风格具有以下优点:
- 减少“复制”操作,例如这是四元组风格:
```
t1 = #1
t2 = #2
t3 = t1 + t2
res = t3
```
SSA 语境下,由于指令就是指本身,指向指令 `t1 = #1` 的指针实际上就是指向常数值 `#1`,因此复制操作是冗余的,上面这段代码可表示为:
```
res = #1 + #2
```


如果你有兴趣,可以阅读 Cliff Click 的 [From Quads to Graphs](http://softlib.rice.edu/pub/CRPC-TRs/reports/CRPC-TR93366-S.pdf),以及 [SSA Book](https://link.springer.com/book/10.1007/978-3-030-80515-9)


## SysY 结构与 Accipit IR 的对应

### 基本结构
## 基本结构

在这一部分我们关注 SysY 语言最基本的结构。

#### 局部变量
### 局部变量

在 Accipit 中,你能看到两种形式的局部变量:

Expand Down Expand Up @@ -259,7 +149,7 @@ let %8 = add %7, 1
let %9 = store %8, %result.addr
```

#### 常数
### 常数

由于 SSA 形式的特性,常量不需要先“复制”给某一个临时的临时变量,而是直接内联在指令中:

Expand All @@ -277,9 +167,9 @@ let %result = add 4, 2

也就是说,指令哪里要用常量,你可以直接把常量插入在那个地方。

#### 函数声明和定义
### 函数声明和定义

##### 函数声明
#### 函数声明

一个函数原型,在 Accipit IR 中能翻译到等价的 `fn` 声明:

Expand All @@ -299,12 +189,12 @@ fn %bar(%value: i64) -> i64;
fn %bar(i64) -> i64;
```

#### 控制流结构
### 控制流结构

类似于汇编,Accipit IR 由一连串指令构成,这些指令一个接一个地顺序执行。
指令按组划分为基本块,每个基本块的终结指令都代表控制流的转移。

##### 简单的 `if-then-else` 分支
#### 简单的 `if-then-else` 分支

让我们来看一个简单的函数,其中包括一个控制流:

Expand Down
File renamed without changes.
File renamed without changes.
19 changes: 14 additions & 5 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
仓库目录结构:

```bash
├── examples/ # SysY 与 Accipit 样例
├── examples/ # 样例
├── docs/ # 实验文档
├── mkdocs.yml
└── src/ # 解释器源码
Expand All @@ -13,7 +13,16 @@
具体来说整个实验分为五个小实验:

- 环境配置与测试用例编写:配置实验环境,学习 SysY 语法。
- 词法分析与语法分析: 实现词法分析和语法分析,将源代码转化为抽象语法树。
- 语义分析:实现符号表,基于抽象语法树进行语义分析。
- 中间代码生成:把分析后的抽象语法树转化为实验定义的中间代码。
- 目标代码生成:将中间代码转化为 RISC-V 64 汇编代码。
- 词法分析与语法分析: 实现词法分析和语法分析,将源代码转化为语法树。
- 语义分析:实现符号表,基于语法树进行语义分析。
- 中间代码生成:把分析后的语法树转化为实验定义的中间代码。
- 目标代码生成:将中间代码转化为 RISC-V 64 汇编代码。

## 致谢

我们对本课程设计中参考的课程与资料表示感谢:

- [全国大学生计算机系统能力大赛](https://compiler.educg.net/#/) 要求实现的 SysY 语言和大量测试用例来自于该大赛。
- [南京大学编译原理](https://cs.nju.edu.cn/changxu/2_compiler/index.html) 我们的部分文档参考了该课程的文档。我们也参考了该课程设计的某些测试用例。
- [北京大学编译原理](https://pku-minic.github.io/online-doc/#/) 我们的部分文档参考了该课程的文档。
- [浙江大学编译原理](https://compiler.pages.zjusct.io/sp24/) 我们的部分文档参考了隔壁班 (刘忠鑫老师) 的文档。
4 changes: 2 additions & 2 deletions docs/middle-ir-gen.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@

## 中间代码的定义

本实验的 IR 是类似 LLVM IR 的 partial SSA 形式,具体的规范请参阅 [Accipit IR 规范](accipit-spec.md).
我们在附录还停供了一些样例:[SysY 结构与 Accipit IR 的对应](appendix.md),为你演示如何从 SysY 前端的高层级结构是如何翻译到 Accipit IR 的。
本实验的 IR 是类似 LLVM IR 的 partial SSA 形式,具体的规范请参阅 [Accipit IR 规范](appendix/accipit-spec.md).
我们在附录还停供了一些样例:[SysY 结构与 Accipit IR 的对应](appendix/sysy-accipit-mapping.md),为你演示如何从 SysY 前端的高层级结构是如何翻译到 Accipit IR 的。

下面这段阶乘的样例代码能帮助你实现一个功能正确(虽然显然欠优化的)的中端代码.

Expand Down
13 changes: 9 additions & 4 deletions mkdocs.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
site_name: 浙江大学编译原理课程实验
site_url: https://accsys.pages.zjusct.io/accipit/
repo_url: https://git.zju.edu.cn/accsys/accipit
repo_name: floatshadow/accipit
repo_name: accsys/accipit
theme:
name: 'material'
language: 'zh'
Expand Down Expand Up @@ -29,10 +29,15 @@ theme:
icon: material/brightness-4
name: Switch to light mode
nav:
- 主页: index.md
- Lab 3 中间代码生成: middle-ir-gen.md
- SysY 语言规范: sysy-spec.md
- Accipit IR 规范: accipit-spec.md
- 附录: appendix.md
- 附录:
- appendix/index.md
- 从四元数到静态单赋值形式: appendix/quads2ssa.md
- SysY 结构与 Accipit IR 的对应: appendix/sysy-accipit-mapping.md
- SysY 语言规范: appendix/sysy-spec.md
- SysY 运行时库: appendix/sysy-runtime.md
- Accipit IR 规范: appendix/accipit-spec.md

plugins:
- search
Expand Down

0 comments on commit 80c8975

Please sign in to comment.