├── .github └── workflows │ └── deploy.yml ├── .gitignore ├── README.md ├── REFERENCE.md ├── SUMMARY.md ├── book.json ├── deploy.sh ├── docs ├── contest │ ├── backend.md │ ├── frontend.md │ ├── intro.md │ └── midend │ │ ├── cp.md │ │ ├── dce.md │ │ ├── ir.md │ │ ├── irgen.md │ │ ├── midend.md │ │ ├── short_circuit.jpg │ │ ├── short_circuit.md │ │ └── ssa.md ├── misc │ └── schedule.md ├── ref │ └── riscv.md ├── step-parser │ ├── example.md │ ├── intro.md │ └── spec.md ├── step0 │ ├── env.md │ ├── errate.md │ ├── intro.md │ ├── pics │ │ └── README.md │ ├── riscv.md │ ├── riscv_env.md │ ├── testing.md │ └── todo.md ├── step1 │ ├── arch.md │ ├── clean │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── antlr.png │ │ ├── antlr2.png │ │ ├── example.dot │ │ ├── example.png │ │ ├── grun.png │ │ ├── main.dot │ │ ├── main.png │ │ ├── parsetree.dot │ │ ├── parsetree.svg │ │ ├── riscv_reg.png │ │ ├── with-ir.svg │ │ └── without-ir.svg │ ├── provided.md │ ├── spec.md │ └── visitor.md ├── step10 │ ├── example.md │ ├── intro.md │ ├── pics │ │ └── program_memory_layout.png │ └── spec.md ├── step11 │ ├── example.md │ ├── guide.md │ ├── intro.md │ ├── pics │ │ └── README.md │ └── spec.md ├── step12 │ ├── example.md │ ├── intro.md │ ├── pics │ │ └── README.md │ ├── spec.md │ └── typesystem.md ├── step13 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── 1.png │ │ └── 2.png │ └── readme.md ├── step14 │ ├── example.md │ └── intro.md ├── step2 │ ├── example.md │ ├── intro.md │ └── spec.md ├── step3 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── exp1.svg │ │ ├── exp2.svg │ │ ├── exp_fixed.svg │ │ ├── ops │ │ ├── ops.svg │ │ └── push_val.svg │ ├── precedence.md │ └── spec.md ├── step4 │ ├── example.md │ ├── intro.md │ ├── pics │ │ └── README.md │ └── spec.md ├── step5 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── call_stack.svg │ │ ├── sf.drawio │ │ ├── sf.svg │ │ └── stack.png │ └── spec.md ├── step6 │ ├── dataflow.md │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── bad_stack_pointer.svg │ │ ├── bad_stack_pointer_2.svg │ │ ├── bad_stack_pointer_3.svg │ │ ├── bad_stack_pointer_4.svg │ │ ├── dataflow.png │ │ ├── flowgraph.png │ │ ├── formula.png │ │ ├── namer.drawio │ │ └── namer.svg │ └── spec.md ├── step7 │ ├── example.md │ ├── intro.md │ ├── manual-parser.md │ ├── pics │ │ └── README.md │ └── spec.md ├── step8 │ ├── example.md │ ├── intro.md │ ├── pics │ │ ├── README.md │ │ ├── dataflow.png │ │ ├── flowgraph.png │ │ ├── formula.png │ │ └── namer.svg │ └── spec.md └── step9 │ ├── example.md │ ├── intro.md │ ├── pics │ ├── 1.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 5.png │ ├── after_call.svg │ ├── after_function_prologue.svg │ ├── after_ret.svg │ ├── before_function_call.svg │ ├── before_function_call_args_pushed.svg │ ├── param.svg │ ├── reg.png │ ├── riscv-regs.png │ └── stack_frame.svg │ └── spec.md ├── howto-gitbook.md ├── readme_for_ta.md └── styles ├── pdf.css └── website.css /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Website 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | build-deploy: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '10' 15 | 16 | - name: Install dependencies and Build 17 | run: | 18 | sudo npm install gitbook-cli -g 19 | gitbook install 20 | gitbook build 21 | 22 | - name: Deploy 23 | uses: JamesIves/github-pages-deploy-action@releases/v3 24 | with: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | BRANCH: gh-pages 27 | FOLDER: _book 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | _book 2 | node_modules 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MiniDecaf 编译实验 2 | 3 | > 实验手册指北:实验文档看起来会有一些长,是因为编译器本身就是一个庞大的系统,我们希望提供尽可能全面的内容来帮助大家理解框架的构成。请大家认真阅读文档,并且尽可能按照文档去动手试一试,而不是直接开始动手写作业。 4 | 5 | ## 实验概述 6 | MiniDecaf [^1] 是一个 C 的子集,去掉了`include/define`等预处理指令,多文件编译支持,以及结构体/指针等语言特性。 本学期的编译实验要求同学们通过多次“思考-实现-重新设计”的过程,一步步实现从简单到复杂的 MiniDecaf 语言的完整编译器,能够把 MiniDecaf 代码编译到 RISC-V 汇编代码。进而深入理解编译原理和相关概念,同时具备基本的编译技术开发能力,能够解决编译技术问题。MiniDecaf 编译实验分为多个 stage,每个 stage 包含多个 step。**每个 step 大家都会完成一个可以运行的编译器**,把不同的 MiniDecaf 程序代码编译成 RISC-V 汇编代码,可以在 QEMU/SPIKE 硬件模拟器上执行。随着实验内容一步步推进,MiniDecaf 语言将从简单变得复杂。每个步骤都会增加部分语言特性,以及支持相关语言特性的编译器结构或程序(如符号表、数据流分析方法、寄存器分配方法等)。下面是采用 MiniDecaf 语言实现的快速排序程序,与 C 语言相同。 7 | 8 | ```c 9 | int qsort(int a[], int l, int r) { 10 | int i = l; int j = r; int p = a[(l+r)/2]; 11 | while (i <= j) { 12 | while (a[i] < p) i = i + 1; 13 | while (a[j] > p) j = j - 1; 14 | if (i > j) break; 15 | int u = a[i]; a[i] = a[j]; a[j] = u; 16 | i = i + 1; 17 | j = j - 1; 18 | } 19 | if (i < r) qsort(a, i, r); 20 | if (j > l) qsort(a, l, j); 21 | return 0; 22 | } 23 | ``` 24 | 25 | 2024 年秋季学期基本沿用了 2023 年秋季学期《编译原理》课程的语法规范。为了贴合课程教学内容,提升训练效果,课程组设计了比较完善的编译器框架,包括词法分析、语法分析、语义分析、中间代码生成、数据流分析、寄存器分配、目标平台汇编代码生成等步骤。每个 step 同学们都会面对一个完整的编译器流程,但不必担心,实验开始的几个 step 涉及的编译器框架知识都比较初级,随着课程实验的深入,将会循序渐进地引入各个编译器功能模块,并通过文档对相关技术进行分析介绍,便于同学们实现相关编译功能模块。 26 | 27 | 从2023年起,课程组增加了大实验环节,大实验是一个**可选**环节。可以参考[大实验参考文档](docs/contest/intro.md)获取更多信息。 28 | 29 | ## 实验起点和基本要求 30 | 31 | 本次实验一共设置 13 个步骤(其中 step 0 和 step 1 为实验框架熟悉,不需要修改框架代码)。后续的 step 2-13 我们将由易到难完成 MiniDecaf 语言的所有特性,由于编译器的边界情况很多,你**只需通过我们提供的正例与负例即可**。 32 | 33 | 我们以 stage 组织实验,各个 stage 组织如下: 34 | 35 |
5 |
6 | program
7 | : function
8 | function
9 | : type Identifier '(' ')' '{' block_item* '}'
10 | type
11 | : 'int'
12 | block_item
13 | : statement
14 | | declaration
15 | statement
16 | : 'return' expression ';'
17 | | expression? ';'
18 | | 'if' '(' expression ')' statement ('else' statement)?
19 | declaration
20 | : type Identifier ('=' expression)? ';'
21 | expression
22 | : assignment
23 | assignment
24 | : conditional
25 | | Identifier '=' expression
26 | conditional
27 | : logical_or
28 | | logical_or '?' expression ':' conditional
29 | logical_or
30 | : logical_and
31 | | logical_or '||' logical_and
32 | logical_and
33 | : equality
34 | | logical_and '&&' equality
35 | equality
36 | : relational
37 | | equality ('=='|'!=') relational
38 | relational
39 | : additive
40 | | relational ('<'|'>'|'<='|'>=') additive
41 | additive
42 | : multiplicative
43 | | additive ('+'|'-') multiplicative
44 | multiplicative
45 | : unary
46 | | multiplicative ('*'|'/'|'%') unary
47 | unary
48 | : primary
49 | | ('-'|'~'|'!') unary
50 | primary
51 | : Integer
52 | | '(' expression ')'
53 | | Identifier
54 |
55 |
56 |
--------------------------------------------------------------------------------
/docs/step0/env.md:
--------------------------------------------------------------------------------
1 | # 实验框架环境配置
2 |
3 | ## Python 实验框架环境配置
4 |
5 | 关于操作系统,助教推荐使用 Linux 环境(如 Ubuntu,Debain 或 Windows 下的 WSL 等),当然你也可以在类 Unix 系统环境(Mac OS)中进行开发。助教不推荐直接在 Window 中搭建开发环境。你需要安装或保证如下软件满足我们的要求:
6 |
7 | 1. python >= 3.9
8 |
9 | 助教**强烈建议**使用类似 [Miniconda](https://docs.conda.io/en/latest/miniconda.html) 或venv的系统管理不同的Python环境。你可以方便地使用miniconda安装最新的Python版本,安装好之后使用pip安装依赖即可。
10 |
11 | 框架本身在 python 3.9 下进行开发,使用了 python 3.9 的新特性,请保证你所使用的 python 版本高于此版本。
12 |
13 | 如果你**没有**使用虚拟环境,可以参考下面的指导。Linux 环境下安装 Python 3 可以尝试如下命令:
14 | ```bash
15 | > sudo add-apt-repository ppa:deadsnakes/ppa
16 | > sudo apt update
17 | > sudo apt install python3
18 | ```
19 |
20 | 此外,如果安装了多个版本的 python,可以通过 `update-alternatives` 命令修改 python 版本使用的优先级,对所有服务器用户都有效,具体用法可参见[这里]( https://medium.com/analytics-vidhya/how-to-install-and-switch-between-different-python-versions-in-ubuntu-16-04-dc1726796b9b)。你可以通过此命令来检查当前优先的 Python3 版本:
21 | ```
22 | > python3 --version
23 | ```
24 |
25 | 框架里已经提供了需要的 python 包列表文件 requirements.txt,你可以通过 pip 命令安装下文提到的 python 依赖包 ply 和 argparse:
26 |
27 | ```bash
28 | $ python3 -m pip install -r ./requirements.txt
29 | ```
30 |
31 | 2. argparse
32 |
33 | 框架使用了 [argparse](https://docs.python.org/zh-cn/3/library/argparse.html) 以处理命令行参数。官方文档中提供了它的[教程](https://docs.python.org/zh-cn/3/howto/argparse.html)。
34 |
35 | 3. ply
36 |
37 | ply是一个自动生成词法分析器和语法分析器的工具,其中ply.lex为词法分析相关的模块而ply.yacc为语法分析相关。可以参考 ply 的[文档](https://www.dabeaz.com/ply/ply.html)。
38 |
39 | 助教在项目中使用 [type hints](https://www.python.org/dev/peps/pep-0483/),如果你习惯在 vscode 中进行开发的话同时推荐使用 [pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) 这一插件。
40 |
41 | 由于 python 的跨平台性,理论上也可以在 Windows 下进行开发。但**不保证Windows和在线测试环境下程序行为的一致性**。
42 |
--------------------------------------------------------------------------------
/docs/step0/errate.md:
--------------------------------------------------------------------------------
1 | # 勘误表
2 |
3 | 在这里我们会列出与实验相关的**勘误**,它会和[问答墙](https://docs.qq.com/doc/DY1hZWFV0T0N0VWph)上的勘误部分保持一致。同学们遇到问题时,请先在勘误表中查找查看是否已有解答。
4 |
5 |
6 |
7 | Q:使用 `pip install -r ./requirements.txt` 命令无法正确安装依赖?
8 |
9 | A:如果你安装了多版本的 python,使用 pip 命令未必会对应 3.9 版本的包管理器。请尝试使用 `python3.9 -m pip install -r ./requirements.txt` 安装依赖。
10 |
11 |
12 |
13 | Q: 代码框架 step7 中,由 multi_nesting.c 生成的以下中间代码无法成功生成目标代码。
14 |
15 | 经过使用 print 法调试,发现是 `_T1` 所对应的寄存器在 `return _T1` 前就被释放了,后端会尝试到栈中寻找 `_T1` 并且不会找到,出现报错:
16 | `utils.error.IllegalArgumentException: error: encounter a non-returned basic block`
17 |
18 | 请问是后端实现上有问题,还是这一部分本来就需要我们自己修改呢?
19 |
20 | A:代码框架的后端除了要修改指令选择部分之外,还需要修改基本块 CFG,可以参见 BruteRegAlloc 的注释里给出的提示。
21 |
22 |
23 |
24 | Q:我怎样才能知道我的提交通过了所有测试用例?
25 |
26 | A:可以通过本地测试或者通过 CI 结果可以判断是否通过了本阶段测例(不过你需要确保你的提交在对应的 branch 上,如 stage1 对应 stage-1 分支)。
27 |
28 |
29 |
30 | Q:如何提交课程报告?
31 |
32 | A:
33 |
34 | 1. 请将实验报告以 pdf 格式提交到 git.tsinghua 自己的仓库中,放在仓库根目录下的 `reports/50 |
1226 |
1226 |
50 |
1176 |
50 |
1176 |
50 |
1176 |
14 |
15 | program
16 | : function
17 |
18 | function
19 | : type Identifier '(' ')' '{' statement '}'
20 |
21 | type
22 | : 'int'
23 |
24 | statement
25 | : 'return' expression ';'
26 |
27 | expression
28 | : Integer
29 |
30 |
31 | # step1 语义规范
32 | **1.1** MiniDecaf 的 int 类型具体指 32 位有符号整数类型,范围 [-2^31, 2^31-1],补码表示。
33 |
34 | **1.2** 编译器应当只接受 [0, 2^31-1] 范围内的整数常量, 不支持负整数常量,如果整数不在此范围内,编译器应当报错。引入负号`-`后,可以用负号配合正整数常量来间接表示负整数常量。
35 |
36 | **1.3** 如果输入程序没有 `main` 函数,编译器应当报错。
37 |
--------------------------------------------------------------------------------
/docs/step1/visitor.md:
--------------------------------------------------------------------------------
1 | # Visitor 模式速成
2 | 编译器的构造中会使用到很多设计模式,Visitor 模式就是常见的一种。 基础的设计模式都在 OOP 课程中覆盖,这里重提一下 Visitor 模式,并以框架中的代码为示例进行介绍。
3 |
4 | 我们知道,编译器里有很多的树状结构。最典型的就是,源程序通过上下文无关文法解析后,得到的抽象语法树。在语义分析和中间表示生成两个步骤中,我们都需要遍历整个抽象语法树。Visitor 模式的目的,就是对遍历树状结构的过程进行封装,本质就是一个 DFS 遍历。
5 |
6 | 让我们考虑 step1 的文法:
7 |
8 | ```
9 | program : function
10 | function : type Identifier '(' ')' '{' statement '}'
11 | type : 'int'
12 | statement : 'return' expression ';'
13 | expression : Integer
14 | ```
15 |
16 | 以这个文法对应的一段 MiniDecaf 代码为示例:
17 |
18 | ```C
19 | int main() {
20 | return 2;
21 | }
22 | ```
23 |
24 | 它会对应如下的 AST 结构:
25 |
26 | ```
27 | program
28 | function
29 | type(int)
30 | identifier(main)
31 | param_list
32 | return
33 | int(2)
34 | ```
35 |
36 | > 我们用缩进表示树结构,其中 program, function, type, identifier, param_list, block, return, int 等均为 AST 上的结点类型。
37 |
38 |
39 | 在框架中,我们有以下的 AST 结点类实现(进行了适当的简略):
40 |
41 | ```python
42 | '''
43 | frontend/ast/node.py
44 | '''
45 | class Node: # 所有 AST 结点的基类
46 | # ...
47 | '''
48 | frontend/ast/tree.py
49 | '''
50 | class Program(ListNode[Union["Function", "Declaration"]]): # 程序,AST 的根结点类型
51 | # ...
52 | class Function(Node): # 函数
53 | # ...
54 | class Statement(Node): # 语句基类
55 | # ...
56 | class Return(Statement): # return 语句
57 | # ...
58 | class TypeLiteral(Node): # 类型基类
59 | # ...
60 | class TInt(TypeLiteral): # 整型
61 | # ...
62 | ```
63 |
64 | 假设在经过了词法分析和语法分析后,我们已经成功将 MiniDecaf 代码转化为了 AST 结构。现在,我们想要编写代码对 AST 进行扫描。很容易写出递归的 DFS 遍历:
65 |
66 | ```python
67 | def dfs(node: Node):
68 | if isinstance(node, Program):
69 | for func in node.functions:
70 | dfs(func)
71 | elif isinstance(node, Function):
72 | # do something for scanning a function node
73 | elif isinstance(node, Return):
74 | # ...
75 | ```
76 |
77 | dfs 函数接收一个结点,根据这个结点的类型进行深度优先遍历。容易看出,dfs 函数根据被遍历的结点类型不同,执行不同的遍历逻辑。 那么我们把这些遍历逻辑封装到一个类里面,就得到了一个最简单的 Visitor。此外,为了便于实现,我们不使用 isinstance 来判断结点类型,而是调用结点自身的一个 accept 函数,并把不同的 visitXXX 函数抽象到一个接口里,各种具体的 Visitor 来实现这个接口。
78 |
79 | ```python
80 | ''' frontend/ast/node.py '''
81 | class Node: # 所有 AST 结点的基类
82 | def accept(self, v: Visitor[T, U], ctx: T) -> Optional[U]:
83 | raise NotImplementedError
84 | ''' frontend/ast/tree.py '''
85 | class Program(ListNode[Union["Function", "Declaration"]]):
86 | # ...
87 | def accept(self, v: Visitor[T, U], ctx: T):
88 | return v.visitProgram(self, ctx)
89 | class Function(Node):
90 | # ...
91 | def accept(self, v: Visitor[T, U], ctx: T):
92 | return v.visitFunction(self, ctx)
93 | # ...
94 | ''' frontend/ast/visitor.py '''
95 | class Visitor(Protocol[T, U]):
96 | def visitOther(self, node: Node, ctx: T) -> None:
97 | return None
98 | def visitProgram(self, that: Program, ctx: T) -> Optional[U]:
99 | return self.visitOther(that, ctx)
100 | def visitFunction(self, that: Function, ctx: T) -> Optional[U]:
101 | return self.visitOther(that, ctx)
102 | # ...
103 | ```
104 |
105 | 之后,如果我们想要编写一种遍历 AST 的方法,可以直接继承 Visitor 类,并在对应结点的 visit 成员方法下实现对应的逻辑。例如,框架中用如下的方法进行符号表构建:
106 |
107 | ```python
108 | class Namer(Visitor[ScopeStack, None]):
109 | def visitProgram(self, program: Program, ctx: ScopeStack) -> None:
110 | # ...
111 | for child in program:
112 | if isinstance(child, Function):
113 | child.accept(self, ctx)
114 | def visitFunction(self, func: Function, ctx: ScopeStack) -> None:
115 | # ...
116 | # ...
117 | ```
118 |
119 | 如果想要访问某个子结点 child,直接调用 child.accept(self, ctx) 即可。
120 |
--------------------------------------------------------------------------------
/docs/step10/example.md:
--------------------------------------------------------------------------------
1 | # step10 实验指导
2 |
3 | 本实验指导使用的例子为:
4 |
5 | ```C
6 | int x = 2024;
7 | int main() { return x; }
8 | ```
9 |
10 | ## 词法语法分析
11 |
12 | 针对全局变量,我们需要新设计 AST 节点来表示它,只需修改根节点的孩子类型即可:原先表示整个 MiniDecaf 程序的根节点只能有函数类型的子节点,现在还可以允许变量声明作为子节点。
13 |
14 | ## 语义分析
15 |
16 | 本步骤引入全局变量,在引入全局变量之后,AST 根结点的直接子结点不只包括函数,还包括全局变量定义。全局变量符号存放在栈底的全局作用域符号表中。在遍历 AST 构建符号表的过程中,栈底的全局作用域符号表一直都存在,不会被弹出。
17 |
18 | ## 中间代码生成
19 |
20 | 经过 Step5 的学习,我们知道局部变量是存储在寄存器或栈中的,可以直接访问。然而,全局变量存储在特别的内存段中,不能直接访问。课程实验建议的加载全局变量方式为:首先加载全局变量符号的地址,然后根据地址来加载数据。因此,需要定义两个中间代码指令,完成全局变量值的加载:
21 |
22 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。
23 |
24 | | 指令 | 参数 | 含义 |
25 | | --- | --- | --- |
26 | | `LOAD` | `T1, offset` | 临时变量 T1 中存储地址,加载与该地址相差 offset 个偏移的内存地址中的数据 |
27 | | `LOAD_SYMBOL` | `symbol` | symbol 为字符串,加载 symbol 符号所代表的地址 |
28 |
29 | 有了上述两条指令,可以将测试用例翻译如下:
30 |
31 | ```
32 | main:
33 | _T0 = LOAD_SYMBOL x
34 | _T1 = LOAD _T0, 0
35 | return T1
36 | ```
37 |
38 | > 需要说明的是,你也可以把两条指令合并为一条指令,直接加载全局变量的值,但分为两条指令的方式可扩展性更好些。
39 |
40 | 请注意,翻译所得的 TAC 代码中没有为全局变量赋予初始值(2024)。可以将变量的初始值存放在变量符号对应的符号表里,在后端代码生成时**通过读取符号表得到初值**。此处给出的只是一种参考实现,大家也可以将全局变量的定义显式翻译为 TAC 代码,这样可以使中端与后端完全解耦。
41 |
42 | ## 目标代码生成
43 |
44 | Step10 中目标代码生成的主要任务有:翻译中间代码,将全局变量放到特定的数据段中。
45 |
46 | 1. 翻译中间代码
47 |
48 | 实际上,我们提供的中间代码设计和 RISC-V 汇编的思想是一致的,RISC-V 汇编中有对应 LOAD 和 LOAD_SYMBOL 的指令,我们直接给出翻译结果:
49 |
50 | ```assembly
51 | main:
52 | la t0, x # _T0 = LOAD_SYMBOL x
53 | lw t1, 0(t0) # _T1 = LOAD _T0, 0
54 | mv a0, t1
55 | ret
56 | ```
57 |
58 | 2. 将全局变量放到特定的数据段中
59 |
60 | 到目前为止,翻译中间代码的方式是有问题的,问题在于,需要加载的 x 变量符号究竟存在哪里,如果所生成的汇编程序不给出 x 的定义,程序是有bug的。实际上,RISC-V 提供了一系列的[汇编指令](https://github.com/TheThirdOne/rars/wiki/Assembler-Directives),用以声明全局变量 x 所对应的数据段。
61 |
62 | 下面给出 RISC-V 用以全局变量声明的汇编指令,其他全局变量的声明只需修改变量名称和初始值即可:
63 |
64 | ```assembly
65 | .data
66 | .globl x
67 | x:
68 | .word 2024
69 | ```
70 |
71 | 上例中,.data 表示输出到 data 数据段;.globl x 声明 x 为全局符号;.word 后是一个 4 字节整数,是 x 符号对应的初始值。
72 |
73 | 按照汇编约定,data 段中存放已初始化的全局变量,未初始化的全局变量则存放在 bss 段中。举例而言,下面的示例将未初始化的全局变量 x 存放到 bss 段中。其中,.space 表示预留一块连续的内存,4 表示存储空间大小为 4 字节。
74 |
75 | ```assembly
76 | .bss
77 | .globl x
78 | x:
79 | .space 4
80 | ```
81 |
82 | # 思考题
83 | 1. 写出 `la v0, a` 这一 RiscV 伪指令可能会被转换成哪些 RiscV 指令的组合(说出两种可能即可)。
84 |
85 | 参考的 RiscV 指令链接:https://github.com/TheThirdOne/rars/wiki/Supported-Instructions
86 |
--------------------------------------------------------------------------------
/docs/step10/intro.md:
--------------------------------------------------------------------------------
1 | # 实验指导 step10:全局变量
2 | step10 我们要支持的是全局变量,语法改动非常简单:
3 |
4 | 8 | 9 | 全局变量和局部变量不同,它不是分配在栈上,而是放在某个固定地址,写在汇编的 .bss 段或 .data 段里。 10 | 访问它也不能通过 `fp` 加偏移量,而是需要通过它的符号加载它的地址,通过它的地址访问它。 11 | > 汇编课上应该讲过,实际中(包括 gcc 和 qemu)使用的可执行文件的格式是 ELF(Executable and Linking Format)。 12 | > .text 是其中存放代码的段(section),.bss 和 .data 都是其中存放数据的段,前者零初始化后者须指定初始值。 13 | > 14 | > 对有兴趣的同学: 15 | > 全局变量地址不是被狭义上的编译器(compiler)确定的,也不是被汇编器(assembler)确定的,而是被链接器(linker)或加载器(loader)确定的。 16 | > 简单的说,狭义上的编译器把源代码变成文本汇编,汇编器把文本汇编给编码到二进制代码,然后通过链接器变成可执行文件,运行时由加载器加载到内存中运行。 17 | > 当然,广义上的编译器就囊括了这所有阶段。 18 | 19 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/5 |
program 6 | : (function | declaration)* 7 |
10 | 11 | 2. 数组的下标操作 12 |7 | declaration 8 |
: type Identifier ('[' Integer ']')* ('=' expression)? ';' 9 |
18 | 19 | step11 难度不大,但有了数组让我们能够写很多有意思的程序了,step11 之前甚至 MiniDecaf 连快速排序都写不了。 20 | 21 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/13 | postfix 14 | : primary 15 | | Identifier '(' expression_list ')' 16 |
| postfix '[' expression ']' 17 |
12 | 13 | 14 | 15 | 数组的传参: 16 | 17 |9 | declaration 10 |
: type Identifier ('[' Integer ']')+ ('=' '{' (Integer (',' Integer)*)? '}')? ';' 11 |
23 | 24 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/18 | function 19 | : type Identifier '(' parameter_list ')' (compound_statement | ';') 20 | parameter_list 21 | : (type Identifier ('[' ']')?(('['Integer']')*)? (',' type Identifier ('[' ']')?(('['Integer']')*)?)*)? 22 |
14 | 15 | 三个操作的语义和 C 以及常识相同,例如 `~0 == -1`,`!!2 == 1`。 16 | 稍微一提,关于按位取反,我们使用补码存储 int;关于逻辑非,只有 0 表示逻辑假,其他的 int 都是逻辑真。 17 | 18 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/7 | expression 8 | : unary 9 | 10 | unary 11 | : Integer 12 | | ('-'|'!'|'~') unary 13 |
8 |28 | 29 | # step2 语义规范 30 | **2.1** 运算符 `-` 的结果是其操作数的相反数。 31 | 32 | **2.2** 运算符 `~` 的结果是其操作数的二进制反码(也就是说,结果中的每一个二进制位是 1 当且仅当其对应的二进制位是 0)。 33 | 34 | **2.3** 当操作数不等于 0 时,逻辑非运算符 `!` 的结果为 0;当操作数等于 0 时,其结果为 1。 35 | 36 | **2.4** MiniDecaf 中,负数字面量不被整体作为一个 token。它被看成是一个取负符号、后面是它的绝对值。 37 | 所以我们无法用字面量表示 `-2147483648`,但可以写成 `-2147483647-1`(待我们加上四则运算后)。 38 | 39 | **2.5** 整数运算越界是**未定义行为**(undefined behavior),即对程序的行为无任何限制。 40 | > 例如 `-(-2147483647-1)` 是未定义行为。这一条规则对于后续 step 引入的运算符也都适用。 41 | > 42 | > 对于含有未定义行为的 C/C++ 程序,在启用优化选项编译时,编译器可能产生意料之外的结果。 43 | -------------------------------------------------------------------------------- /docs/step3/example.md: -------------------------------------------------------------------------------- 1 | # step3 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | 1+3 7 | ``` 8 | 9 | ## 词法语法分析 10 | 在 step3 中,我们引入了算术运算,因此需要引入新的抽象语法树节点: 11 | 12 | | 节点 | 成员 | 含义 | 13 | | --- | --- | --- | 14 | | `Binary` | 左操作数 `lhs`,右操作数 `rhs`,运算类型 `op` | 二元运算 | 15 | 16 | > 对有兴趣的同学:虽然 `-2` 和 `2-3` 里面的 `-` 意义不同,但 lexer 不知道这点(parser 才知道),所以它们都会用同样的 token kind `-` 表示。 17 | > 但有时,可能需要后续阶段告诉 lexer(或 parser)一些信息,最经典的例子是 [“typedef-name identifier problem”](https://en.wikipedia.org/wiki/Lexer_hack)。 18 | 19 | ## 语义分析 20 | 21 | 同 Step2。 22 | 23 | ## 中间代码生成 24 | 与一元操作类似,针对加法,我们需要设计一条中间代码指令来表示它,给出的参考定义如下: 25 | 26 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 27 | 28 | | 指令 | 参数 | 作用 | 29 | | ----- | ------- | -------------- | 30 | | `ADD` | `T0,T1` | 将两个参数相加 | 31 | 32 | 因此,测例可以翻译成如下的中间代码: 33 | 34 | ```assembly 35 | _T0 = 1 36 | _T1 = 3 37 | _T2 = ADD _T0, _T1 38 | ``` 39 | 40 | ## 目标代码生成 41 | 42 | step3 目标代码生成步骤的关键点与 step2 相同,针对中间代码指令,选择合适的 RISC-V 指令来完成翻译工作。 43 | 44 | ```assembly 45 | li t0, 1 46 | li t1, 3 47 | add t2, t0, t1 48 | ``` 49 | 50 | # 思考题 51 | 52 | 1. 我们知道“除数为零的除法是未定义行为”,但是即使除法的右操作数不是 0,仍然可能存在未定义行为。请问这时除法的左操作数和右操作数分别是什么?请将这时除法的左操作数和右操作数填入下面的代码中,分别在你的电脑(请标明你的电脑的架构,比如 x86-64 或 ARM)中和 RISCV-32 的 qemu 模拟器中编译运行下面的代码,并给出运行结果。(编译时请不要开启任何编译优化) 53 | 54 | ```c 55 | #include9 | program 10 | : function 11 | 12 | function 13 | : type Identifier '(' ')' '{' statement '}' 14 | 15 | type 16 | : 'int' 17 | 18 | statement 19 | : 'return' expression ';' 20 | 21 |
expression 22 | : unary 23 | 24 | unary 25 | : Integer 26 | | ('-'|'!'|'~') unary 27 |
26 | 27 | 新特性的语义、优先级、结合性和 C 以及常识相同,例如 `1+2*(4/2+1) == 7`。 28 | 29 | 我们这种表达式语法写法可能比较繁琐,但它有几个好处: 30 | 1. 和 [C17 标准草案](../../REFERENCE.md)保持一致 31 | 2. 把优先级和结合性信息直接编码入语法里,见[优先级和结合性](./precedence.md)一节。 32 | 33 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/7 | expression 8 | : additive 9 | 10 | additive 11 | : multiplicative 12 | | additive ('+'|'-') multiplicative 13 | 14 | multiplicative 15 | : unary 16 | | multiplicative ('*'|'/'|'%') unary 17 | 18 | unary 19 | : primary 20 | | ('-'|'~'|'!') unary 21 | 22 | primary 23 | : Integer 24 | | '(' expression ')' 25 |
20 | 21 | 当然,它是有歧义的,你也可以自己试试,如果只有这些产生式的话,上面的两个表达式都可以解析成正确或者错误的结果。所以如果想基于这个规范来实现语法分析器,就必须告诉语法分析工具这些操作符的优先级和结合性是什么。 22 | 23 | 之后每一步给出的语法都是没有歧义,本身就能体现优先级和结合性的。如果你确实想借助优先级和结合性来实现,需要两个步骤: 24 | 25 | 1. 把我们给出的语法规范转化成类似上面这样“更模糊”,有歧义的语法规范。我们相信这个方向的转化应该是容易的。 26 | 2. 指定每个操作符的优先级和结合性。可以参考[https://en.cppreference.com/w/c/language/operator_precedence](https://en.cppreference.com/w/c/language/operator_precedence),它给出了C语言操作符的优先级和结合性,因为我们的MiniDecaf语言是C语言的一个子集,所以这张表格也足够我们的语言使用了。 27 | -------------------------------------------------------------------------------- /docs/step3/spec.md: -------------------------------------------------------------------------------- 1 | # 规范 2 | 每个步骤结尾的 **规范** 一节都会对这个步骤中的新特性给出规范,方便大家查阅。 3 | 4 | # step3 语法规范 5 | 灰色部分表示相对上一节的修改。 6 | 7 |13 | expression 14 | : expression ('+'|'-') expression 15 | | expression ('*'|'/'|'%') expression 16 | | ('-'|'~'|'!') expression 17 | | Integer 18 | | '(' expression ')' 19 |
8 |40 | 41 | # step3 语义规范 42 | 43 | **3.1** 二元操作符 `*` 的结果是操作数的乘积。 44 | 45 | **3.2** 二元操作符 `/` 的结果是第一个操作数除以第二个操作数所得的商的整数部分(即所谓“向零取整”),二元操作符 `%` 的结果是第一个操作数除以第二个操作数所得的余数。在两种操作中,如果第二个操作数为 0,则其行为都是未定义的。当 `b` 不为 0 时,表达式 `(a/b)*b + a%b` 应该等于 `a`。 46 | 47 | **3.3** 二元操作符 `+` 的结果是操作数的和。 48 | 49 | **3.4** 二元操作符 `-` 的结果是第一个操作数减去第二个操作数所得的差。 50 | 51 | **3.5** 除非特别声明,子表达式求值顺序是**未规定行为**(unspecified behavior),即其行为可以是多种合法的可能性之一。也就是说,以任意顺序对子表达式求值都是合法的。 52 | 例如:执行 `int a=0; (a=1)+(a=a+1);` 之后 a 的值是未规定的(待我们加上变量和赋值运算符后,这个问题才会产生真正切实的影响)。 53 | -------------------------------------------------------------------------------- /docs/step4/example.md: -------------------------------------------------------------------------------- 1 | # step4 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | 1<2 7 | ``` 8 | 9 | ## 词法语法分析 10 | 11 | 本 step 中引入的运算均为二元运算,在 step3 中引入的二元运算节点中进行修改即可。 12 | 13 | ## 语义分析 14 | 15 | 同 Step2。 16 | 17 | ## 中间代码生成 18 | 针对小于符号,我们显然需要设计一条中间代码指令来表示它,给出的参考定义如下: 19 | 20 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 21 | 22 | | 指令 | 参数 | 含义 | 23 | | --- | --- | --- | 24 | | `LT` | `T0,T1` | 给出 `T09 | program 10 | : function 11 | 12 | function 13 | : type Identifier '(' ')' '{' statement '}' 14 | 15 | type 16 | : 'int' 17 | 18 | statement 19 | : 'return' expression ';' 20 | 21 |
expression 22 | : additive 23 | 24 | additive 25 | : multiplicative 26 | | additive ('+'|'-') multiplicative 27 | 28 | multiplicative 29 | : unary 30 | | multiplicative ('*'|'/'|'%') unary 31 | 32 | unary 33 | : primary 34 | | ('-'|'~'|'!') unary 35 | 36 | primary 37 | : Integer 38 | | '(' expression ')' 39 |
13 | 14 | 2. 逻辑与 `&&`、逻辑或 `||` 15 |6 | equality 7 | : relational 8 | | equality ('=='|'!=') relational 9 | 10 | relational 11 | : additive 12 | | relational ('<'|'>'|'<='|'>=') additive
26 | 27 | 新特性的语义、优先级、结合性和 C 以及常识相同,例如 `1<3 == 2<3 && 5>=2` 是逻辑真(int 为 `1`)。 28 | 但特别注意,C 中逻辑运算符 `||` 和 `&&` 有短路现象,我们不要求。 29 | 30 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/16 | expression 17 | : logical_or 18 | 19 | logical_or 20 | : logical_and 21 | | logical_or '||' logical_and 22 | 23 | logical_and 24 | : equality 25 | | logical_and '&&' equality
8 |56 | 57 | 58 | # step4 语义规范 59 | 60 | **4.1** 关系操作符 `<`(小于)、`>`(大于)、`<=`(小于等于)和`>=`(大于等于)的结果取决于两个操作数是否满足它们所指定的关系,当满足时结果为 1,当不满足时结果为 0。 61 | > 关系操作符可能导致表达式的含义与数学文献中常见的含义不同,例如 `0<1<2` 的含义与 `(0<1)<2` 相同,即“如果 0 小于 1,那么判断是否有 1 小于 2,否则判断是否有 0 小于 2”。 62 | 63 | **4.2** 判等操作符 `==`(等于)和 `!=`(不等于)类似于关系操作符,结果取决于两个操作数是否满足它们所指定的关系,当满足时结果为 1,当不满足时结果为 0。但判等操作符的优先级比关系操作符更低。对于任意一对操作数,这两个操作符中有且仅有一个结果为 1。 64 | > 其优先级的设定会导致其含义在某些时候可能会反直观,例如,`0 < 1 == 2 < 3` 的运算结果为 1。 65 | 66 | **4.3** 当操作数都非 0 时,逻辑与操作符 `&&` 的结果为 1;否则其结果为 0。 67 | 68 | **4.4** 当操作数有一个非 0 时,逻辑或操作符 `||` 的结果为 1;否则其结果为 0。 69 | 70 | **4.5** 逻辑操作符 `||` 和 `&&` 依然遵循语义规范 3.5,即其操作数的求值顺序是未指定行为。 71 | 换言之,我们不对逻辑表达式的短路求值做要求,可以将操作数两个以任意顺序计算出,再计算逻辑操作的结果。 72 | 73 | -------------------------------------------------------------------------------- /docs/step5/example.md: -------------------------------------------------------------------------------- 1 | # step5 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 2024; 8 | return x; 9 | } 10 | ``` 11 | 12 | ## 词法语法分析 13 | 针对局部变量定义和赋值操作,我们需要设计 AST 节点来表示它,给出的参考定义如下(框架中已经提供): 14 | 15 | | 节点 | 成员 | 含义 | 16 | | --- | --- | --- | 17 | | `TInt` | 无 | 整型 | 18 | | `Identifier` | 名称 `value` | 标识符(用于表示变量名) | 19 | | `Assignment` | 同 `Binary` | 赋值运算 | 20 | | `Declaration` | 类型 `var_t`,标识符 `ident`,初始表达式 `init_expr` | 变量声明 | 21 | 22 | 请注意,赋值操作是一种特殊的**二元运算**,因此可以将它合并到 `Binary` 节点里,也可以单独设置一类节点继承 `Binary` 类来处理它。 23 | 24 | ## 语义分析 25 | 26 | 从本节开始,我们需要在语义分析阶段对局部变量的规范进行检查。具体来说,我们需要名为符号表的数据结构。符号表的实现已经在框架中给出。因此,你只需要修改语义分析部分的代码,在必要时调用符号表的接口即可。 27 | 28 | 在符号表构建过程中,我们要按照语句顺序,逐一访问所有变量定义声明。在访问变量声明时,我们需要为该变量赋予一个变量符号,并将它存入符号表中。由于变量不能重复声明,在定义变量符号前需要在符号表中检查是否有同名符号。 29 | 30 | 类似地,在访问表达式时,如果遇到变量的使用,我们也需要在符号表中检查,避免使用未声明的变量。例如,如果我们将测例修改为: 31 | 32 | ```C 33 | int main() { 34 | int x = 2024; 35 | return x + y; 36 | } 37 | ``` 38 | 39 | 那么在扫描到加法操作的 AST 结点时,会依次检查该操作的两个操作数 x 和 y。这两个操作数均为变量标识符,因此我们需要到符号表中搜索 x 和 y 对应的符号。符号 x 可以在符号表中找到(我们在扫描 `int x = 2024;` 这条语句后已经为其定义),而 y 无法找到,因此编译器需要在扫描到 y 对应的结点时报错。 40 | 41 | 符号表总是和作用域相关的。例如,在 C 语言中,我们可以在全局作用域中定义名为 "a" 的全局变量,同时在 main 函数中定义名为 "a" 的局部变量,这并不产生冲突。不过由于本节还无需支持全局变量和块语句,同学们不用考虑这一点,只考虑 main 函数作用域对应的单张符号表即可。 42 | 43 | 此外,在本节中,我们引入了赋值操作。赋值可以看作一种特殊的二元运算,但需要注意,赋值号左侧必须为一个**左值**。具体来说,同学们需要检查赋值号左侧只能是变量名标识符。在 step11 中,我们会将左值的范围进一步包括数组元素。 44 | 45 | 对应到框架代码上: 46 | 47 | `frontend/symbol` 目录下为符号的实现。其中 `symbol.py` 为符号类的基类,`varsymbol.py` 为变量符号。在本节中,同学们只需要考虑变量符号即可。 48 | 49 | `frontend/scope` 目录下为符号表的实现。其中 `scope.py` 为作用域类,在本节中由于只有一个局部作用域,因此无需考虑作用域栈。同学们只需要新建一个 Scope 对象,用以维护 main 函数中所有出现过的变量符号即可。 50 | 51 | ## 中间代码生成 52 | 53 | 我们首先来看本节指导用例所对应的中间代码: 54 | 55 | ```assembly 56 | main: 57 | _T1 = 2024 58 | _T0 = _T1 59 | return _T0 60 | ``` 61 | 62 | 针对赋值操作,我们显然需要设计一条中间代码指令来表示它,给出的参考定义如下: 63 | 64 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 65 | 66 | | 指令 | 参数 | 含义 | 67 | | --- | --- | --- | 68 | | `ASSIGN` | `T0,T1` | 临时变量的赋值 | 69 | 70 | 从中间代码可以看出,尽管我们引入了变量的概念,但是在比较低级的中间代码上,数据的存储和传递仍然是基于虚拟寄存器进行的。由于 MiniDecaf 语言中的基本类型只有 int 型,而 TAC 里的临时变量也是 32 位整数,因此,我们可以把 MiniDecaf 局部变量和 TAC 临时变量对应起来。 71 | 72 | 在扫描到 `int x = 2024;` 这条语句时,中间代码先把立即数 2024 加载到临时变量 _T1 中,然后再把 _T1 的值赋给临时变量 _T0,此时 _T0 已经成为了变量 x 的“替身”。每次需要用到变量 x 的值时,我们都会去访问 _T0。例如,测例中直接用返回 _T0 代替了返回变量 x 的值。因此,为了在后续使用变量 x 时能快速找到 _T0 这个临时变量,在符号表中存储 x 这个符号时,应当为该符号设置一个成员,存储 x 对应的临时变量。每当在 AST 上扫描到一个变量标识符结点时,我们都直接调用该变量对应的临时变量作为结点的返回值。 73 | 74 | 请注意 `frontend/symbol/varsymbol.py` 中,变量符号的定义里有该变量对应的 TAC 临时变量成员。 75 | 76 | ## 目标代码生成 77 | 78 | 本节指导用例对应如下 RISC-V 汇编代码: 79 | 80 | ```assmbly 81 | .text 82 | .global main 83 | main: 84 | li t1, 2024 85 | mv t0, t1 # 我们使用 mv 指令来翻译中间表示里的 ASSIGN 指令 86 | mv a0, t0 87 | ret 88 | ``` 89 | 90 | ### 简单的启发式寄存器分配算法 91 | 92 | 在中间代码中,我们使用了虚拟寄存器来存储变量的值。如果所使用的虚拟寄存器的个数,超过了目标机器实际拥有的物理寄存器数目,将无法生成正确的目标代码。此时,需要采用**寄存器分配算法**,调度和分配数目有限的物理寄存器资源,保证所有**临时变量(虚拟寄存器或伪寄存器)**都有合适的物理寄存器与之对应。在程序执行的任何时刻,都需要保证不会出现寄存器分配冲突,即两个同时有效且将被引用的临时变量(虚拟寄存器)被分配到同一个物理寄存器中,寄存器分配冲突将造成程序运行结果的错误。然而,寄存器分配问题是**NP 完备问题**(可以从 3-SAT 问题归约),这意味着对于一个含有大量临时变量的程序,为了获得最优寄存器分配方案,编译器将耗费可观的计算时间用于寄存器分配。因此,考虑到执行效率问题,实际的编译器实现中一般采用启发式算法。 93 | 94 | 实验框架中所采用的启发式寄存器分配算法基于**活跃性分析**。为避免一次性介绍过多的知识,将在 Step6 详细介绍活跃性分析的相关理论。大家目前只需要了解,活跃性分析是为了求解每个临时变量是否会在程序某点之后被引用,如果被引用,这个临时变量就是活跃的。 95 | 基于活跃性分析的启发式寄存器分配算法的基本思路:针对每一条 TAC 指令(例如 _T2 = ADD _T1, _T0),对于每个源操作数对应的临时变量(本例中 _T1 和 _T0),我们检查该临时变量是否已经存放在物理寄存器中,如果不是,则分配一个物理寄存器,并从**栈帧**中把该临时变量加载到寄存器中;对于目标操作数对应的临时变量(本例中的 _T2),如果该临时变量没有对应的物理寄存器,则为其分配一个新的物理寄存器。寄存器分配过程中,将为临时变量和为该变量分配的物理寄存器之间建立一种关联关系。 96 | 在分配寄存器时,首先检查是否存在空闲的寄存器(即尚未跟任何临时变量建立关联关系的寄存器),有则选择该寄存器作为分配结果。否则,检查有没有这样的寄存器,其所关联的临时变量在当前位置已经不是活跃变量了,这说明该寄存器所保存的数据未来不会被用到,可以回收使用这个寄存器而不用担心引起数据错误。一种可能的情况是,所有寄存器所关联的变量都是活跃的,即不存在空闲的寄存器。此时,将把某个寄存器所关联的暂时不用的变量存到栈帧(内存的一部分)中,腾出这个寄存器,这也称为溢出(spill)到内存。所腾空的寄存器是随机选取的,因此,所采用的寄存器分配算法有些暴力,存在进一步优化空间。 97 | 98 | 在实验框架中已经给出寄存器分配算法的代码,集中在 `backend/reg/bruteregalloc.py` 中,主要有以下几个函数: 99 | 100 | 1. `accept`:根据每个函数的 DFG(数据流图)进行寄存器分配,寄存器分配结束后生成相应汇编代码。 101 | 2. `bind`:将一个 Temp(临时变量)与寄存器绑定。 102 | 3. `unbind`:将一个 Temp(临时变量)与相应寄存器解绑定。 103 | 4. `localAlloc`:根据活跃变量信息对一个 BasicBlock(基本块)内的指令进行寄存器分配。 104 | 5. `allocForLoc`:每一条指令进行寄存器分配。 105 | 6. `allocRegFor`:根据活跃变量信息决定为当前 Temp(临时变量)分配哪一个寄存器。 106 | 107 | ### 栈帧 108 | 109 | 上面的描述中提到,在分配寄存器的时候**从栈帧中加载数据**,以及**将暂时不用的变量存储到栈帧**中,接下来介绍栈帧的概念。 110 | 111 | 1. 栈帧的概念 112 | 113 | 在汇编语言课程学习中,大家应该已经接触到**栈帧**的概念,下面简单回顾一下。在程序执行过程中,每次调用和执行一个函数,都会在栈上开辟一块新的存储空间,这块存储空间就叫做“栈帧”。栈帧中存放了函数执行所需的各种数据,包括需要临时保存的局部变量、在栈上临时申请的存储空间(如数组,在 Step11 中介绍)、被调用者负责保存的寄存器等。栈帧是函数正确调用和执行的保证。 114 | 115 | > 需要注意的是,由于我们目前只支持一个 main 函数,直到 Step9 才会有多函数支持。所以现在关于栈帧的讨论,就只针对 main 函数的栈帧,并且集中于临时变量的存储和加载。 116 | 117 | 假设当前函数被某个函数调用,下图给出当前函数的栈帧。如图所示,当前函数的栈帧由被调用者负责保存的寄存器、保存的临时变量以及局部变量三个部分组成,fp 指向当前栈帧的栈底,sp 指向当前栈帧的栈顶,fp 和 sp 之间的部分就是当前函数的栈帧。当前实验步骤中,需要关注的是**临时变量保存区域**,正是在这个区域中,保存了为腾空物理寄存器而取出的临时变量(仍然活跃的临时变量)。值得一提的是,临时变量保存区域中还保存了基本块出口处仍活跃的临时变量(关于基本块的概念,将在 Step6 介绍,在当前的步骤不需要考虑)。 118 | 119 |  120 | 121 | 2. 栈帧的建立与销毁 122 | 123 | 栈帧是函数运行所需要的上下文的一部分,在进入函数的时候需要建立对应的栈帧,在退出函数的时候需要销毁其对应的栈帧。栈帧对于函数的运行非常重要。那么程序在运行的过程中如何建立和销毁栈帧呢?实际上,建立栈帧的操作是由编译器生成代码完成的。在每个函数的起始位置,由编译器生成的用于建立栈帧的那段汇编代码称为函数的 **prologue**。prologue 所做的事情包括:分配栈帧空间和保存相应寄存器的值。相应的,在每个函数的末尾,用于销毁栈帧的那段汇编代码称为函数的 **epilogue**。epilogue 所做的事情包括:设置返回地址,回收栈帧空间,以及从当前被调用函数过程返回 124 | 125 | 貌似创建和销毁栈帧是一个大工程?实际不然,确定栈帧只需要维护好两个寄存器,sp 和 fp,它们分别保存当前栈帧的栈顶地址和栈底地址。当新的函数被调用时,需要把旧栈帧的栈底地址(fp)保存起来,用旧栈帧的栈顶地址(sp)表示新栈帧的栈底地址(新fp)。不难看出,新老栈帧在栈内存中是连续的存储空间。此外,每个函数体中需要分配的局部变量以及需要保存的临时变量在编译过程中是可知的。因此,栈帧的大小在编译期可以计算得出,即存储寄存器的空间,临时变量存储空间与局部变量空间三者之和。在求得栈帧大小之后,可以通过修改栈顶指针(sp)的值来分配恰当的栈帧空间。 126 | 127 | 3. 一个例子 128 | 129 | ```C 130 | #include9 | program 10 | : function 11 | 12 | function 13 | : type Identifier '(' ')' '{' statement '}' 14 | 15 | type 16 | : 'int' 17 | 18 | statement 19 | : 'return' expression ';' 20 | 21 |
expression 22 | : logical_or 23 | 24 | logical_or 25 | : logical_and 26 | | logical_or '||' logical_and 27 | 28 | logical_and 29 | : equality 30 | | logical_and '&&' equality 31 | 32 | equality 33 | : relational 34 | | equality ('=='|'!=') relational 35 | 36 | relational 37 | : additive 38 | | relational ('<'|'>'|'<='|'>=') additive 39 |40 | additive 41 | : multiplicative 42 | | additive ('+'|'-') multiplicative 43 | 44 | multiplicative 45 | : unary 46 | | multiplicative ('*'|'/'|'%') unary 47 | 48 | unary 49 | : primary 50 | | ('-'|'~'|'!') unary 51 | 52 | primary 53 | : Integer 54 | | '(' expression ')' 55 |
12 |37 | 38 | 我们要增加和变量相关的语义检查:变量不能重复声明,不能使用未声明的变量。 39 | 40 | **请将你的作业放置在分支`stage-2`下,你可以通过`git checkout -b stage-2`创建一个新的分支并继承当前分支的修改。** 41 | 42 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/13 |
function 14 | : type Identifier '(' ')' '{' statement* '}' 15 |16 | statement 17 | : 'return' expression ';' 18 || expression? ';' 19 | | declaration 20 | 21 | declaration 22 | : type Identifier ('=' expression)? ';' 23 | 24 | expression 25 | : assignment 26 | 27 | assignment 28 | : logical_or 29 | | Identifier '=' expression 30 |31 | 32 | primary 33 | : Integer 34 | | '(' expression ')' 35 || Identifier 36 |
7 |65 | 66 | # step5 语义规范 67 | 68 | **5.1** 每一条变量声明(定义)指定了对标识符的解释和属性。当变量被定义时,应当有一块存储空间为这个变量所保留。当变量声明之后,若与这个变量的名称相同的标识符作为操作数(operand)出现在一个表达式中时,其就应被指派(designate)为这个变量。 69 | 70 | **5.2** 变量的初始化表达式指定了变量的初始值。 71 | 72 | **5.3** 同一个标识符应只能作为至多一个变量的名字,即是说,不允许声明重名变量。 73 | 74 | **5.4** 对未声明的变量的使用是错误。 75 | 76 | **5.5** 没有被初始化的(局部)变量的值是不确定的。 77 | > 在初始化表达式中,正在被初始化的变量已被声明,但其值尚未被初始化。 78 | > 例如,`int a = a + 1;`,这样一条声明在语义上等价于 `int a; a = a + 1;` 79 | 80 | **5.6** 局部变量的名字可以为 `main`。 81 | 82 | **5.7** 赋值运算 `=` 的左操作数必须是一个**可修改的左值**(modifiable lvalue)。**左值**(lvalue)即一个会被指派为某个变量的表达式,如在 `int a; a = 1;` 中,`a` 即是一个会被指派为变量的表达式。左值**可修改**是指被指派的变量不能是一个左值数组。 83 | > 就 step5 来说,这一点其实几乎已经被语法保证,因为其 `=` 的左边只能是一个标识符,只需再要求其是一个已经声明过的变量的名字即可。 84 | 85 | **5.8** 在赋值运算(`=`)中,右操作数的值会被存在左操作数所指派的变量中。 86 | 87 | **5.9** 赋值表达式的结果,为赋值运算完成后左操作数所指派的变量的值,但这个结果本身并非左值。 88 | 89 | **5.10** 一个函数中可以有任意多条 `return` 语句。 90 | 91 | **5.11** 当 `main` 函数执行至 `}` 时,应终止执行并返回 0。 92 | -------------------------------------------------------------------------------- /docs/step6/dataflow.md: -------------------------------------------------------------------------------- 1 | # 数据流分析 2 | 3 | 编译优化的基础是**数据流分析**。 4 | 5 | 基本块(basic block)和控制流图(control-flow graph)是用于进行上述分析的数据结构。 6 | 7 | 以下讲述数据流分析的内容中,所有的 CondBranch 指令为条件跳转指令,Branch 指令为跳转指令。 8 | 9 | ## 基本块 10 | 11 | 基本块是指一段这样的代码序列: 12 | 13 | 1. 除出口语句外基本块中不含任何的 Branch、Beqz(条件为假时跳转)、Bnez(条件为真时跳转)或者 Return 等跳转语句(但可以包含 Call 语句)。 14 | 15 | 2. 除入口语句外基本块中不含任何的 Label 标记,即不能跳转到基本块中间。 16 | 17 | 3. 在满足前两条的前提下含有最多的连续语句,即基本块的头尾再纳入一条语句将会违反上面两条规则。 18 | 19 | 下面的例子中,代码中不同的基本块被标以不同的颜色: 20 | 21 |  22 | 23 | 也就是说,基本块内的代码执行过程总是从基本块入口开始,到基本块出口结束的,中间不会跳到别的地方或者从别的地方跳进来。 24 | 25 | ## 控制流图 26 | 27 | 控制流图是一个有向图:它以基本块作为结点,如果一个基本块 A 执行完之后,有可能跳转到另一个基本块 B,则图中包含从 A 对应结点到 B 对应结点的有向边。对于以 Branch 语句或者任何非跳转语句结尾的基本块,其后继只有一个结点;对于以 CondBranch 语句结尾的基本块,其后继含有两个结点,分别对应跳转条件为真和假的情况。不难想像,控制流图的有向边组成的每一个环路都对应着程序中的一个循环结构。由于该图给出程序控制流的各种可能执行路径,因此也称为控制流图。 28 | 29 | 为进行编译优化,建立控制流图是必不可少的一步。已知一个操作序列,如何根据这个操作序列建立对应的控制流图呢?通常分为两步进行: 30 | 31 | 1. 划分基本块。 32 | 33 | 2. 建立基本块之间的连接关系。 34 | 35 | 基本块的划分算法比较简单:从头到尾扫描操作序列,当遇到以下情况时结束当前基本块,并开始一个新的基本块建立过程: 36 | 37 | 1. 当遇到一个 Label 标记而且存在跳转语句跳转到这个行号时。 38 | 39 | 2. 当遇到 Branch、CondBranch 或者 Return 等跳转语句时。 40 | 41 | 整个操作序列扫描完毕后,我们就成功建立了所有基本块。 42 | 43 | 在划分好基本块之后,需要从头到尾依次扫描所有的基本块建立控制流图: 44 | 45 | 1. 如果当前基本块以 Branch 结尾,则在当前基本块与所跳转到的目标基本块之间加入一条有向边。 46 | 47 | 2. 如果当前基本块以 CondBranch 结尾,则在当前基本块和跳转条件成立与不成立的目标基本块之间分别加入一条有向边(共 2 条边)。 48 | 49 | 3. 如果当前基本块以 Return 结尾,则不需要加入新的边。 50 | 51 | 在所有的基本块都扫描完毕后,即建立了控制流图。基于控制流图,可以进行控制流分析。 52 | 53 | 上面例子对应的控制流图如下: 54 | 55 |  56 | 57 | ## 活跃变量和活跃变量方程 58 | 59 | 从编译器中端出来的中间代码中,我们对 TAC 中使用的临时变量的个数并没有做任何限制。但是在实际机器中,物理寄存器的数量是有限的。 60 | 61 | 因此我们需要想办法把这些无限多的临时变量“塞”到有限个物理寄存器里面:如果两个临时变量不会在同一条指令中被用到,那么我们可以让这两个临时变量使用同一个物理寄存器(把一部分当前指令用不到的临时变量保存到栈上)。 62 | 63 | 根据这样的原则,大多数的临时变量都可以用有限的几个物理寄存器对应起来,而“塞不下”的那些临时变量,则可以暂时保存到内存里面(因为访问内存的时间比访问寄存器的时间多得多,因此临时变量应尽可能一直存放在物理寄存器中,尽量不要 spill 到栈上)。 64 | 65 | 由于一个物理寄存器在确定的时刻只能容纳一个临时变量,因此为了把若干个变量塞到同一个物理寄存器里面,我们需要知道各个临时变量分别在哪条指令以后不会再被用到(以便腾出当前临时变量占用的物理寄存器给别的临时变量)。此时我们需要用到活性分析(liveness analysis),或者称为“活跃变量分析”。 66 | 67 | 一个临时变量在某个执行点是活的(也叫“活跃”、live),是指该临时变量在该执行点处具有的值会在这个执行点以后被用到,换句话说,就是在该执行点到给这个临时变量重新赋值的执行点之间存在着使用到这个临时变量的语句。活性分析是指分析每一个临时变量在程序的每一个执行点处的活跃情况,通常是通过计算出每个执行点处的活跃变量集合来完成。 68 | 69 | 下面代码中每行语句右边都给出了执行完该语句后的活跃变量集合: 70 | 71 | | TAC 代码 | 活跃变量集合 | 72 | | ----------------- | --------------- | 73 | | `_T0 = 4` | {_T0} | 74 | | `_T1 = 3` | {_T0, _T1} | 75 | | `_T2 = _T0 * _T1` | {_T0} | 76 | | `_T3 = _T0 * _T0` | {_T0, _T3} | 77 | | `_T2 = _T3 * _T3` | {_T0, _T2, _T3} | 78 | | `_T2 = _T0 * _T2` | {_T2, _T3} | 79 | | `_T1 = _T2 * _T3` | {_T1} | 80 | | `return _T1` | 空集 | 81 | 82 | 一般来说,活性分析是通过求解活跃变量方程来完成的。为了介绍活跃变量方程的概念, 我们需要先引入下面四种针对基本块的集合: 83 | 84 | 1. Def 集合:一个基本块的 Def 集合是在这个基本块内被定值的所有变量。所谓的定值 (definition),可以理解为给变量赋值,例如加法语句给目标变量定值等(注意:Store 语句不给任何变量定值,Load 语句则会给对应变量定值)。 85 | 86 | 2. LiveUse 集合:一个基本块的 LiveUse 集合是在这个基本块中所有在定值前就被引用过的变量,包括了在这个基本块中被引用到但是没有被定值的那些变量。 87 | 88 | 3. LiveIn 集合:在进入基本块入口之前必须是活跃的那些变量。 89 | 90 | 4. LiveOut 集合:在离开基本块出口的时候是活跃的那些变量。 91 | 92 | 其中 Def 和 LiveUse 是基本块本身的属性,对每个基本块从后往前遍历基本块内的指令便可以求出。 93 | 94 | 有了基本块的这四个集合的概念,我们给出控制流图中每个基本块满足的活跃变量方程: 95 | 96 |  97 | 98 | 该方程说的是一个基本块的 LiveOut 集合是其所有后继基本块的 LiveIn 集合的并集,而且 LiveIn 集合是 LiveUse 集合的变量加上 LiveOut 集合中去掉 Def 集合以后的部分。 99 | 100 | 这个方程的直观意义是: 101 | 102 | 1. 一个基本块的任何一个后继基本块入口处活跃的变量在这个基本块的出口必须也是活跃的。 103 | 104 | 2. 在一个基本块入口处需要活跃的变量是在该基本块中没有定值就被使用的变量,以及在基本块出口处活跃但是基本块中没有定值过的变量(因为它们的初值必定是在进入基本 块之前就要具有的了)。 105 | 106 | 根据这个方程,我们可以通过迭代更新的办法求出每个基本块的 LiveIn、LiveOut 集合,以下是求解的伪代码: 107 | 108 | ``` 109 | for i <- 1 to N do compute Def[B_i] and LiveUse[B_i]; 110 | for i <- 1 to N do LiveIn[B_i] <- phi ; 111 | changed <- true; 112 | while (changed) do { 113 | changed <- false; 114 | for i <- N downto 1 do { 115 | LiveOut[B_i] <- Union (LiveIn[s]) where s belongs to succ(B_i) ; 116 | NewLiveIn <- Union (LiveUse[B_i], (LiveOut[B_i] – Def[B_i])); 117 | if (LiveIn[B_i] != NewLiveIn) then { 118 | changed <- true; 119 | LiveIn[B_i] <- NewLiveIn; 120 | } 121 | } 122 | } 123 | ``` 124 | 125 | 获得了每个基本块的 LiveIn 和 LiveOut 集合以后,我们需要进一步地计算基本块内每个 TAC 语句的 LiveIn 和 LiveOut 集合。如果我们把基本块内所有 TAC 语句分别看成是一个独立的基本块,则不难想像,前面提到的活跃变量方程仍然有效,不同之处在于,一个基本块对应的 “控制流图” 有以下三种特点: 126 | 127 | 1. 每个节点的出度都是 1,也就是说 LiveOut(B) = LiveIn(Succ(B))。 128 | 129 | 2. 由于每个结点只含有一个语句,因此其 Def 集要么是空集,要么只含有一个元素。 130 | 131 | 3. 由于每个结点对应的语句里所引用的所有变量在使用的时候都未在该基本块中经过定值,其 LiveUse 集合就是源操作数对应的所有变量。 132 | 133 | 基于上面三个特点,已经求出基本块的 LiveOut 集合的前提下我们只需要在每个基本块内从后往前遍历基本块内的指令就可以对每条基本块内指令求出 LiveIn、LiveOut。 134 | -------------------------------------------------------------------------------- /docs/step6/example.md: -------------------------------------------------------------------------------- 1 | # step6 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 1; 8 | { 9 | x = 2; 10 | int x = 3; 11 | } 12 | x = 4; 13 | return x; 14 | } 15 | ``` 16 | 17 | ## 词法语法分析 18 | 针对块语句,我们需要设计 AST 节点来表示它,给出的参考定义如下: 19 | 20 | | 节点 | 成员 | 含义 | 21 | | --- | --- | --- | 22 | | `Block` | 子语句列表 `children` | 语句块 | 23 | 24 | ## 语义分析 25 | 26 | 从 Step6 开始,我们需要考虑作用域和代码块。简而言之,一份代码中可能有多个代码块的嵌套,因此作用域开始出现了层次结构。例如,在示例中,尽管 main 函数里定义了变量 x,但随后我们开启了一个新的代码块。在这个代码块中,赋值语句 `x = 2;` 中的 x 就是指 main 作用域中定义的 x,而随后通过 `int x = 3;` 我们定义了另一个变量 x,这个 x 只在内部大括号括起的作用域内生效。 27 | 28 | 在 Step5 中,我们只维护了 main 的作用域,所有符号都在这个作用域的符号表中维护。现在,为了维护层次嵌套的作用域,我们引入了**作用域栈**(Scope Stack)这个数据结构。在进行符号表构建的扫描过程中,我们需要动态维护作用域栈,保存当前扫描结点所在的从内到外所有作用域。每次我们开启一个代码块时,要新建一个作用域并压栈;而当退出代码块时,要弹栈关闭此作用域。 29 | 30 | 接下来针对上述代码示例,讲述作用域栈的维护方式。首先,栈底有一个全局作用域,其符号表里只有 main 函数。由于目前不需要考虑函数和全局变量,可以暂时忽略全局作用域。进入 main 函数时,开启一个局部作用域,在扫描 `int x = 1;` 时定义变量符号 x,并将其加入栈顶作用域对应的符号表中。如下所示: 31 | 32 | | 作用域栈 | 符号表 | 33 | | ------------------ | ------------------- | 34 | | 全局作用域(栈底) | 函数 main(可忽略) | 35 | | 局部作用域(栈顶) | 变量 x | 36 | 37 | 接下来,扫描到一个局部代码块,由此建立一个局部作用域并压栈。在扫描 `x = 2;` 时,我们需要分析 x 这个变量对应着哪个作用域里的符号。此时的作用域栈是这样的: 38 | 39 | | 作用域栈 | 符号表 | 40 | | ------------------ | ------------------- | 41 | | 全局作用域(栈底) | 函数 main(可忽略) | 42 | | 局部作用域 | 变量 x | 43 | | 局部作用域(栈顶) | 空 | 44 | 45 | 对变量x的查找从栈顶开始,由上向下依次查找对应的符号表,直至找到变量 x 为止。由于在栈顶作用域对应的符号表中不存在变量符号 x,于是向下继续查找。在 main 函数对应的作用域中,可以找到变量符号 x。因此,语句 `x = 2;` 中的 x 对应 main 函数作用域里定义的变量 x。 46 | 47 | 接下来,当扫描到语句 `int x = 3;` 时,定义了另一个变量 x。此时,只需要在栈顶作用域中查找该变量是否存在。若不存在,即在符号表中加入对应符号。此时的作用域栈如下: 48 | 49 | | 作用域栈 | 符号表 | 50 | | ------------------ | ------------------- | 51 | | 全局作用域(栈底) | 函数 main(可忽略) | 52 | | 局部作用域 | 变量 x | 53 | | 局部作用域(栈顶) | 变量 x | 54 | 55 | 请务必注意上表中的两个变量 x 是**不同的变量**。 56 | 57 | 接下来,退出代码块,将其对应的作用域弹出栈,此时的作用域栈如下: 58 | 59 | | 作用域栈 | 符号表 | 60 | | ------------------ | ------------------- | 61 | | 全局作用域(栈底) | 函数 main(可忽略) | 62 | | 局部作用域(栈顶) | 变量 x | 63 | 64 | 最后,扫描语句 `x = 4;` 时,从栈顶作用域符号表查找 x,所找到的变量 x 为 main 作用域定义的 x 变量。 65 | 66 | ## 中间代码生成 67 | 68 | 本步骤中无须新增新的 TAC 指令。 69 | 70 | 让我们来看看示例所对应的 TAC 代码: 71 | 72 | ```asm 73 | main: 74 | _T1 = 1 75 | _T0 = _T1 # int x = 1; 76 | _T2 = 2 77 | _T0 = _T2 # x = 2 78 | _T4 = 3 79 | _T3 = _T4 # int x = 3; 80 | _T5 = 4 81 | _T0 = _T5 # x = 4; 82 | return _T0 83 | ``` 84 | 85 | 显然,两个代码块里的变量 x 是不同的变量,因此它们分别对应着不同的临时变量。其中,_T0 对应着 main 作用域里的 x,而 _T3 则对应着内层代码块定义的变量 x。只要同学们在符号表构建阶段把每个变量和正确作用域的变量符号关联起来,这一步就非常简单了:找到对应变量符号,使用该符号对应的临时变量即可。 86 | 87 | ## 目标代码生成 88 | 89 | 不需要新增新的中间代码指令。 90 | 91 | 代码框架需要同学们对寄存器分配相关的 CFG 的内容进行细微修改。具体来说,需要在 `backend/dataflow/cfg.py` 中添加基本块是否可达的判断。在寄存器分配算法 `backend/reg/bruteregalloc.py` 的注释中,我们给出了提示,如果一个基本块不可达,那么无须为它分配寄存器。 92 | 93 | 94 | # 实现提示 95 | 96 | 1. 在 step5 中,namer/typer 遍历时的上下文信息(参数 ctx)是单一的作用域。到了 step 6,你需要按照实验指导书中描述,**把上下文信息改成“作用域栈”**。也即定义 `class Namer(Visitor[Scope, None])` 应改为 `class Namer(Visitor[YourType, None])`,其中 `YourType` 是你的作用域栈类型,你可以任意命名它。我们推荐把这个类的定义放在 `frontend/scope/` 下。class Typer 也需要如上改动。 97 | 98 | 2. 之前 step5 的全局唯一的作用域可以被当作“函数作用域使用”,在 visitFunction 入栈。然后在新的 visitBlock 中,再进一步将局部作用域压栈。最后,在所有这些方法的末尾,不要忘了把对应作用域退栈。 99 | 100 | 3. 当只有一个作用域时,“不可以定义新变量a”就意味着当前“可以获取变量a的值”,反之亦然,所以“定义变量”和“获取变量”的检查都可以用 `Scope.lookup` 实现。但有了多个作用域之后,就出现了“既可以拿到a的值,也可以重新定义一个a”的情况。这需要重新考虑 Typer / Namer 中的每一个 `Scope.lookup` ,看她们是否需要换成新函数。 101 | 102 | 4. 后续 stage-4 时,你需要一个机制来检查 break/continue 语句是否在一个循环内。这可以通过修改 namer/typer 中的对应结点来实现。另外,别忘了循环本身也是一个作用域! 103 | 104 | 5. 后续如果你选做“全局变量”部分,可以在 Namer 和 Typer 的 transform 方法中先将全局作用域加入栈底,再往上才是 visitFunction 的函数作用域。 105 | 106 | # 思考题 107 | 1. 请画出下面 MiniDecaf 代码的控制流图。 108 | ```c 109 | int main(){ 110 | int a = 2; 111 | if (a < 3) { 112 | { 113 | int a = 3; 114 | return a; 115 | } 116 | return a; 117 | } 118 | } 119 | ``` 120 | -------------------------------------------------------------------------------- /docs/step6/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step6:作用域和块语句 2 | step6 我们要增加块语句的支持。 3 | 4 | 虽然块语句语义不难,就是把多个语句组成一个块,每个块都是一个作用域。 5 | 随之而来一个问题是:不同变量可以重名了。 6 | 重名的情况包括作用域内部声明覆盖(shadowing)外部声明,以及不相交的作用域之间的重名变量。 7 | 因此,变量名不能唯一标识变量了,同一个变量名 `a` 出现在代码不同地方可能标识完全不同的变量。 8 | 我们需要在符号表构建的过程中,确定 AST 中出现的每个变量名分别对应那个变量。 9 | 10 | 语法上改动不大 11 | 12 |8 | program 9 | : function 10 | 11 |
function 12 | : type Identifier '(' ')' '{' statement* '}' 13 |14 | type 15 | : 'int' 16 | 17 | statement 18 | : 'return' expression ';' 19 || expression? ';' 20 | | declaration 21 | 22 | declaration 23 | : type Identifier ('=' expression)? ';' 24 | 25 | expression 26 | : assignment 27 | 28 | assignment 29 | : logical_or 30 | | Identifier '=' expression 31 |32 | logical_or 33 | : logical_and 34 | | logical_or '||' logical_and 35 | 36 | logical_and 37 | : equality 38 | | logical_and '&&' equality 39 | 40 | equality 41 | : relational 42 | | equality ('=='|'!=') relational 43 | 44 | relational 45 | : additive 46 | | relational ('<'|'>'|'<='|'>=') additive 47 | 48 | additive 49 | : multiplicative 50 | | additive ('+'|'-') multiplicative 51 | 52 | multiplicative 53 | : unary 54 | | multiplicative ('*'|'/'|'%') unary 55 | 56 | unary 57 | : primary 58 | | ('-'|'~'|'!') unary 59 | 60 | primary 61 | : Integer 62 | | '(' expression ')' 63 || Identifier 64 |
26 | 27 | 语义检查我们也要修改了,只有在同一个作用域里,变量才不能重复声明。 28 | 当然,如果变量在使用前还是必须先被声明。 29 | 30 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/13 |
function 14 | : type Identifier '(' ')' compound_statement 15 |16 |compound_statement 17 | : '{' block_item* '}' 18 |19 | statement 20 | : 'return' expression ';' 21 || compound_statement 22 | block_item 23 | : statement 24 | | declaration25 |
8 |77 | 78 | 79 | # step6 语义规范 80 | 81 | **6.1** 根据其声明的位置,每一个标识符都属于一个作用域。目前我们有两种作用域:文件级和块级。如果是在块中声明,则标识符其声明所属的块的作用域中,例如局部变量;否则标识符在文件级(全局)作用域中,例如全局变量。 82 | 83 | **6.2** (更新 5.6)如果一个标识符在两个作用域里面,这两个作用域必然是嵌套的,即一个内层作用域完全被另一个外层作用域所覆盖。且在内层作用域中,外层作用域里该标识符所指派(designate)的变量或函数是不可见的。 84 | > 在初始化表达式中,其正在初始化的变量已被声明,会隐藏(shadow)外层作用域的同名变量,但其值不确定。例如在下面的代码片段中,`a + 1` 的值是不确定的。 85 | > ``` 86 | > int a = 1; 87 | > { 88 | > int a = a + 1; 89 | > } 90 | > ``` 91 | 92 | **6.3** (更新 5.3)对于同一个标识符,在同一个作用域中至多有一个声明。 93 | 94 | **6.4** (更新 5.4)使用不在当前开作用域中的变量名是不合法的。 95 | 96 | -------------------------------------------------------------------------------- /docs/step7/example.md: -------------------------------------------------------------------------------- 1 | # step7 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | int main() { 7 | int x = 1; 8 | if (x) x = 2; else x = 3; 9 | return x; 10 | } 11 | ``` 12 | 13 | ## 词法语法分析 14 | 针对 if 语句,我们需要设计 AST 节点来表示它,给出的参考定义如下(框架中已经提供): 15 | 16 | | 节点 | 成员 | 含义 | 17 | | --- | --- | --- | 18 | | `If` | 分支条件 `cond`,真分支 `then`,假分支 `otherwise` | if 分支语句 | 19 | 20 | 仿照 if 节点,还需要类似地实现条件表达式节点。 21 | 22 | ### 悬吊 else 问题 23 | 24 | 这一节引入的 if 语句既可以带 else 子句也可以不带,但这会导致语法二义性:`else` 到底和哪一个 `if` 结合? 25 | 例如 `if(a) if(b) c=0; else d=0;`,到底是 `if(a) {if(b) c=0; else d=0;}` 还是 `if(a) {if(b) c=0;} else d=0;`? 26 | 这个问题被称为 **悬吊 else(dangling else)** 问题。 27 | 28 | 如果程序员没有加大括号,那么我们需要通过一个规定来解决歧义。 29 | 我们人为规定:`else` 和最近的 `if` 结合,也就是说上面两种理解中只有前者合法。 30 | 为了让 parser 能遵守这个规定,一种方法是设置产生式的优先级,优先选择没有 else 的 if。 31 | 按照这个规定,parser 看到 `if(a) if(b) c=0; else d=0;` 中第一个 if 时,选择没有 else 的 if; 32 | 而看到第二个时只能选择有 else 的 if ,也就使得 `else d=0;` 被绑定到 `if(b)` 而不是 `if(a)` 了。 33 | 34 | > 需要说明的是 bison 默认在 shift-reduce conflict 的时候选择shift,从而对悬挂else进行就近匹配。 35 | 36 | ## 语义分析 37 | 38 | 本步骤中语义分析没有特别需要增加的内容,只需要在扫描到 if 语句和条件表达式时递归地访问其子结点即可。请注意 if 语句**不总是**有 else 分支,所以在递归到子结点时,请先判断子结点是否存在。 39 | 40 | ## 中间代码生成 41 | 从本步骤开始,由于 MiniDecaf 程序出现了分支结构,我们需要开始考虑跳转语句了。在 Step1-4 中,TAC 代码中的标签只有标志 main 函数入口这一个功能。而现在,我们需要使用标签来指示跳转指令的目标位置。我们用 _Lk 来表示跳转用标签,以此和函数入口标签区分开来。 42 | 43 | 为了实现 if 语句,我们需要设计两条中间代码指令,分别表示条件跳转和无条件跳转,给出的参考定义如下: 44 | 45 | > 请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。 46 | 47 | | 指令 | 参数 | 作用 | 48 | | --- | --- | --- | 49 | | `BEQZ` | `T0, Label` | 若 T0 的值为0,则跳转到 LABEL 标签处 | 50 | | `JUMP` | `Label` | 跳转到 LABEL 标签处 | 51 | 52 | 现在让我们来看看示例所对应的 TAC 代码: 53 | 54 | ```assembly 55 | main: 56 | _T1 = 1 57 | _T0 = _T1 58 | BEQZ _T0, _L1 59 | _T2 = 2 60 | _T0 = _T2 61 | JUMP _L2 62 | _L1: 63 | _T3 = 3 64 | _T0 = _T3 65 | _L2: 66 | return _T0 67 | ``` 68 | 69 | 在这段 TAC 代码中,x 对应的临时变量为 _T0。如果 x 的值为真(不等于0),那么应当执行 then 分支 `x = 2;`,否则执行 else 分支 `x = 3;`。因此,我们设置了两个跳转标签 _L1 和 _L2,分别表示 else 分支开始位置和整个 if 语句的结束位置。如果 x 为假,那么应当跳转到 _L1 处,我们使用一条 BEQ 指令来执行。如果 x 为真,那么按顺序执行 then 分支的代码,并在该分支结束时,用一条 JMP 指令跳转到 if 语句的结束位置,从而跳过 else 分支。在 TAC 生成过程中,每当扫描到 if 语句时,都需要调用 TAC 的底层接口,新建两个跳转标签,并按照这种方式生成中间代码。 70 | 71 | 当然,如果一条 if 语句没有 else 分支,那么只需要一个跳转标签即可。例如我们将例子中的 if 语句修改为 `if (x) x = 2;`,则对应的 TAC 代码可简化为: 72 | 73 | ```assembly 74 | main: 75 | _T1 = 1 76 | _T0 = _T1 77 | BEQ _T0, _L1 78 | _T2 = 2 79 | _T0 = _T2 80 | _L1: 81 | return _T0 82 | ``` 83 | 84 | 同样地,条件表达式也可以使用类似的方法完成中间代码生成。要注意的是,条件表达式是一种特殊的**表达式**,因此有返回值。同学们在实现的时候不要忘记为其分配临时变量。 85 | 86 | ## 目标代码生成 87 | Step7 中目标代码生成主要是指令的选择以及 label 的声明,RISC-V 提供了与中间代码中 BEQZ 和 JUMP 类似的指令: 88 | 89 | ```assembly 90 | step7: # RISC-V 汇编标签 91 | beqz t1, step7 # 如果 t1 为 0,跳转到 step7 标签处 92 | j step7 # 无条件跳转到 step6 标签处 93 | ``` 94 | 95 | # 思考题 96 | 97 | 1. 我们的实验框架里是如何处理悬吊 else 问题的?请简要描述。 98 | 99 | 2. 在实验要求的语义规范中,条件表达式存在短路现象。即: 100 | 101 | ```c 102 | int main() { 103 | int a = 0; 104 | int b = 1 ? 1 : (a = 2); 105 | return a; 106 | } 107 | ``` 108 | 109 | 会返回 0 而不是 2。如果要求条件表达式不短路,在你的实现中该做何种修改?简述你的思路。 110 | 111 | # 总结 112 | 本节主要就是引入了跳转,后面 Step8 循环语句还会使用。 113 | 114 | -------------------------------------------------------------------------------- /docs/step7/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step7: 2 | step7 我们要支持条件语句,包括 if 语句和条件表达式(又称三元/三目表达式,ternary expression)。 3 | 4 | 语法上的改动是: 5 | 6 | 1. if 表达式 7 |9 | program 10 | : function 11 | 12 |
function 13 | : type Identifier '(' ')' compound_statement 14 |15 | type 16 | : 'int' 17 | 18 |compound_statement 19 | : '{' block_item* '}' 20 | 21 | block_item 22 | : statement 23 | | declaration 24 |25 | 26 | statement 27 | : 'return' expression ';' 28 || compound_statement 29 |30 | declaration 31 | : type Identifier ('=' expression)? ';' 32 | 33 | expression 34 | : assignment 35 | 36 | assignment 37 | : conditional 38 | | Identifier '=' expression 39 | 40 | conditional 41 | : logical_or 42 | | logical_or '?' expression ':' conditional 43 | 44 | logical_or 45 | : logical_and 46 | | logical_or '||' logical_and 47 | 48 | logical_and 49 | : equality 50 | | logical_and '&&' equality 51 | 52 | equality 53 | : relational 54 | | equality ('=='|'!=') relational 55 | 56 | relational 57 | : additive 58 | | relational ('<'|'>'|'<='|'>=') additive 59 | 60 | additive 61 | : multiplicative 62 | | additive ('+'|'-') multiplicative 63 | 64 | multiplicative 65 | : unary 66 | | multiplicative ('*'|'/'|'%') unary 67 | 68 | unary 69 | : primary 70 | | ('-'|'~'|'!') unary 71 | 72 | primary 73 | : Integer 74 | | '(' expression ')' 75 | | Identifier 76 |
13 | 14 | 2. 条件表达式 15 |8 | statement 9 | : 'return' expression ';' 10 | | expression? ';' 11 |
| 'if' '(' expression ')' statement ('else' statement)? 12 |
23 | 24 | if 语句的语义和 C 语言相同,注意条件表达式优先级只比赋值高。 25 | 26 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/16 | assignment 17 | : conditional 18 | | Identifier '=' expression
19 | conditional 20 | : logical_or 21 | | logical_or '?' expression ':' conditional 22 |
8 |78 | 79 | > 注意:`if` 的 `then` 分支和 `else` 分支需要是一个语句(statement)而非声明(declaration)。 80 | > 例如 `if (1) int a;` 不是合法的 MiniDecaf 程序。 81 | 82 | # step7 语义规范 83 | 84 | **7.1** 条件表达式会先对第一个操作数求值,再根据其值选择计算第二个或第三个操作数。当且仅当第一个操作数的值不等于 0,我们会对第二个操作数求值。当且仅当第一个操作数的值等于 0,我们会对第三个操作数求值。当第一个操作数的值为 0 时,条件表达式的求值结果为第二个操作数所求得的值;当第一个操作数的值非 0 时,条件表达式的求值结果为第三个操作数所求得的值。 85 | > 不论选择第二个操作数或者是第三个操作数去求值,都必须首先计算完第一个操作数,之后才能开始第二个或第三个操作数的求值计算。 86 | 87 | **7.2** 对于 if 语句而言,当控制条件不等于 0 时,会执行第一个子句;当控制条件等于 0 时,如果有 else 分支,就会执行第二个语句,否则整个 if 语句的执行便已经完成。 88 | 89 | **7.3** 如果出现悬吊 `else`(dangling else),要求 `else` 优先和最接近的没有匹配 `else` 的 `if` 匹配。 90 | > 例如 `if (0) if (0) ; else ;` 等价于 `if (0) { if (0) ; else; }` 而非 `if (0) { if (0) ; } else ;`。 91 | -------------------------------------------------------------------------------- /docs/step8/example.md: -------------------------------------------------------------------------------- 1 | # step8 实验指导 2 | 3 | 本实验指导使用的例子为: 4 | 5 | ```C 6 | for (int i = 0; i < 5; i = i + 1) 7 | break; 8 | // 后续语句 ... 9 | ``` 10 | 11 | ## 词法语法分析 12 | 13 | 针对循环语句和 break/continue 语句,我们需要设计 AST 节点来表示它,给出的参考定义如下: 14 | 15 | | 节点 | 成员 | 含义 | 16 | | --- | --- | --- | 17 | | `While` | 循环条件 `cond`,循环体 `body` | while 循环语句 | 18 | | `For` | 初始语句 `init`,循环条件 `cond`,更新语句 `update`,循环体 `body` | for 循环语句 | 19 | | `Break` | 无 | break 语句 | 20 | | `Continue` | 无 | continue 语句 | 21 | 22 | 其中,while 和 break 语句的实现已经在框架中给出,同学们可以参考并实现 for 和 continue 语句。 23 | 24 | ## 语义分析 25 | 26 | 本步骤语义分析阶段的处理方式和 Step7 中的 if 语句相类似,但是请额外注意以下两点: 27 | 28 | 1. for 循环要自带一个作用域。在示例里,`for (int i = 0; i < 5; i = i + 1)` 语句里定义的循环变量处于一个独自的作用域里。这也就是说,我们可以在循环体内部定义同名变量。如果我们把示例修改为:`for (int i = 0; i < 5; i = i + 1) { int i = 0; }` 这也是合法的 MiniDecaf 程序。因此,在符号表构建阶段,扫描到 for 结点时,不要忘记开启一个局部作用域。 29 | 30 | 2. break 和 continue 语句必须位于循环体内部才合法。因此,在扫描过程中,需要记录当前结点位于多少重循环内。扫描到 break 和 continue 结点时,若当前不处于任何循环内,则报错。 31 | 32 | ## 中间代码生成 33 | 34 | 本步骤中没有需要新增的 TAC 指令。不过为了实现循环语句,需要仔细地考虑如何将 MiniDecaf 循环语句翻译成 TAC 的分支跳转指令。由于 while 循环可以看作 for 循环的特例,我们选择了 for 循环作为示例。 35 | 让我们先来看看示例对应的 TAC 代码: 36 | 37 | ```assembly 38 | _T1 = 0 39 | _T0 = _T1 # int i = 0; 40 | _L1: # begin label 41 | _T2 = 5 42 | _T3 = LT _T0, _T2 43 | BEQZ _T3, _L3 # i < 5; 44 | _L2: # loop label 45 | _T4 = 1 46 | _T5 = ADD _T0, _T4 47 | _T0 = _T5 # i = i + 1; 48 | JUMP _L1 49 | _L3: # break label 50 | # 后续指令 ... 51 | ``` 52 | 53 | 为了实现所有可能的跳转,对每个 for 循环我们都需要定义三个跳转标签:begin, loop 和 break。它们的作用如下: 54 | 55 | 1. begin 标签(示例中的 _L1)是循环体的开始位置。初次进入循环时,从这个标签的位置开始执行,并判断循环条件是否满足,若不满足,则跳转到 break 标签(示例中的 _L3)处。 56 | 57 | 2. loop 标签(示例中的 _L2)是执行 continue 语句时应当跳转到的位置。 58 | 59 | 3. break 标签是整个循环结束后的位置。如果循环条件不满足,或者执行了 break 语句,那么应当跳转到此处,执行循环之后的指令。 60 | 61 | 请注意,示例给出的只是一种循环语句**参考实现**,同学们也可以设计自己的实现方法。 62 | 63 | 由于循环语句可以嵌套,所以 TAC 语句生成过程中需要动态维护 loop 标签和 break 标签,这样才能确定每一条 break 和 continue 语句跳转到何处。因此,在 TAC 生成时,需要使用栈结构维护从内到外所有的 loop 标签和 break 标签。 64 | 65 | `utils/tacgen/tacgen.py` 里的 `TACFuncEmitter` 类里实现了维护 TAC 生成时需要的上下文信息的功能。同学们可以在这个类中增加对循环所需的 break/continue 标签的维护。 66 | 67 | ## 目标代码生成 68 | 69 | 由于不需要增加新的中间代码指令,本步骤中目标代码生成模块没有新的内容。除非之前步骤的实现有误,否则这个步骤应该不会出现错误。 70 | 71 | # 思考题 72 | 73 | 1. 将循环语句翻译成 IR 有许多可行的翻译方法,例如 while 循环可以有以下两种翻译方式: 74 | 75 | 第一种(即实验指导中的翻译方式): 76 | 77 | + `label BEGINLOOP_LABEL`:开始下一轮迭代 78 | + `cond 的 IR` 79 | + `beqz BREAK_LABEL`:条件不满足就终止循环 80 | + `body 的 IR` 81 | + `label CONTINUE_LABEL`:continue 跳到这 82 | + `br BEGINLOOP_LABEL`:本轮迭代完成 83 | + `label BREAK_LABEL`:条件不满足,或者 break 语句都会跳到这儿 84 | 85 | 第二种: 86 | 87 | + `cond 的 IR` 88 | + `beqz BREAK_LABEL`:条件不满足就终止循环 89 | + `label BEGINLOOP_LABEL`:开始下一轮迭代 90 | + `body 的 IR` 91 | + `label CONTINUE_LABEL`:continue 跳到这 92 | + `cond 的 IR` 93 | + `bnez BEGINLOOP_LABEL`:本轮迭代完成,条件满足时进行下一次迭代 94 | + `label BREAK_LABEL`:条件不满足,或者 break 语句都会跳到这儿 95 | 96 | 从执行的指令的条数这个角度(`label` 不算做指令,假设循环体至少执行了一次),请评价这两种翻译方式哪一种更好? 97 | 98 | 2. 我们目前的 TAC IR 中条件分支指令采用了单分支目标(标签)的设计,即该指令的操作数中只有一个是标签;如果相应的分支条件不满足,则执行流会继续向下执行。在其它 IR 中存在双目标分支(标签)的条件分支指令,其形式如下: 99 | 100 | ```assembly 101 | br cond, false_target, true_target 102 | ``` 103 | 104 | 其中`cond`是一个临时变量,`false_target`和`true_target`是标签。其语义为:如果`cond`的值为0(假),则跳转到`false_target`处;若`cond`非0(真),则跳转到`true_target`处。它与我们的条件分支指令的区别在于执行流总是会跳转到两个标签中的一个。 105 | 106 | 你认为中间表示的哪种条件分支指令设计(单目标 vs 双目标)更合理?为什么?(言之有理即可) 107 | -------------------------------------------------------------------------------- /docs/step8/intro.md: -------------------------------------------------------------------------------- 1 | # 实验指导 step8:循环语句 2 | step8 我们要增加对循环语句,以及 break/continue 的支持: 3 | 4 |9 | program 10 | : function 11 | 12 | function 13 | : type Identifier '(' ')' '{' block_item* '}' 14 | 15 | type 16 | : 'int' 17 | 18 | compound_statement 19 | : '{' block_item* '}' 20 | 21 | block_item 22 | : statement 23 | | declaration 24 | 25 | statement 26 | : 'return' expression ';' 27 | | compound_statement 28 |
| expression? ';' 29 | | 'if' '(' expression ')' statement ('else' statement)? 30 |31 | declaration 32 | : type Identifier ('=' expression)? ';' 33 | 34 | expression 35 | : assignment 36 | 37 |assignment 38 | : conditional 39 | | Identifier '=' expression 40 | 41 | conditional 42 | : logical_or 43 | | logical_or '?' expression ':' conditional 44 |45 | logical_or 46 | : logical_and 47 | | logical_or '||' logical_and 48 | 49 | logical_and 50 | : equality 51 | | logical_and '&&' equality 52 | 53 | equality 54 | : relational 55 | | equality ('=='|'!=') relational 56 | 57 | relational 58 | : additive 59 | | relational ('<'|'>'|'<='|'>=') additive 60 | 61 | additive 62 | : multiplicative 63 | | additive ('+'|'-') multiplicative 64 | 65 | multiplicative 66 | : unary 67 | | multiplicative ('*'|'/'|'%') unary 68 | 69 | unary 70 | : primary 71 | | ('-'|'~'|'!') unary 72 | 73 | primary 74 | : Integer 75 | | '(' expression ')' 76 | | Identifier 77 |
17 | 18 | 循环语句的语义和 C 语言相同,注意检查 break/continue 不能出现在循环外。 19 | 20 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/5 | statement 6 | : 'return' expression ';' 7 | | expression? ';' 8 | | 'if' '(' expression ')' statement ('else' statement)? 9 | | compound_statement 10 |
| 'for' '(' expression? ';' expression? ';' expression? ')' statement 11 | | 'for' '(' declaration expression? ';' expression? ')' statement 12 | | 'while' '(' expression ')' statement 13 | | 'break' ';' 14 | | 'continue' ';' 15 |16 |
6 |program 7 | : function* 8 | 9 | function 10 | : type Identifier '(' parameter_list ')' (compound_statement | ';') 11 |12 |parameter_list 13 | : (type Identifier (',' type Identifier)*)? 14 |15 | 16 | 17 | 2. 我们还需要支持函数调用: 18 |19 |expression_list 20 | : (expression (',' expression)*)? 21 |22 |unary 23 | : postfix 24 | | ('-'|'~'|'!') unary 25 | 26 | postfix 27 | : primary 28 | | Identifier '(' expression_list ')' 29 |30 | 31 | 32 | 语义检查部分,我们需要检查函数的重复定义、检查调用函数的实参(argment)和形参(parameter)的个数类型一致。我们不支持 void 返回类型,这可以通过忽略函数的 int 返回值实现。 33 | 34 | 我们只接受 pdf 格式的实验报告,你需要将报告放在仓库的 `./reports/.pdf`,比如 stage 5 的实验报告需要放在 `stage-5` 这个 branch 下的 `./reports/stage-5.pdf`。注意报告的标题是 `stage-5` 而不是 `step-9`。 35 | 36 | 你需要: 37 | 1. 改进你的编译器,支持本节引入的新特性,通过相关测试。 38 | 2. 完成实验报告(具体要求请看实验指导书的首页)。实验报告中需要包括: 39 | * 你的学号姓名 40 | * 简要叙述,为了完成这个 stage 你做了哪些工作(即你的实验内容) 41 | * 指导书上的思考题 42 | * 如果你复用借鉴了参考代码或其他资源,请明确写出你借鉴了哪些内容。*并且,即使你声明了代码借鉴,你也需要自己独立认真完成实验。* 43 | * 如有代码交给其他同学参考,也必须在报告中声明,告知给哪些同学拷贝过代码(包括可能通过间接渠道传播给其他同学)。 44 | -------------------------------------------------------------------------------- /docs/step9/pics/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/1.png -------------------------------------------------------------------------------- /docs/step9/pics/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/2.png -------------------------------------------------------------------------------- /docs/step9/pics/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/3.png -------------------------------------------------------------------------------- /docs/step9/pics/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/4.png -------------------------------------------------------------------------------- /docs/step9/pics/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/5.png -------------------------------------------------------------------------------- /docs/step9/pics/reg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/reg.png -------------------------------------------------------------------------------- /docs/step9/pics/riscv-regs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/decaf-lang/minidecaf-tutorial/61db51aadd2d3ef98afbdb6e6a1e61d5980a7c52/docs/step9/pics/riscv-regs.png -------------------------------------------------------------------------------- /howto-gitbook.md: -------------------------------------------------------------------------------- 1 | ## 运行步骤 2 | 3 | 1. 安装 gitbook: 4 | ```bash 5 | npm install gitbook-cli -g 6 | ``` 7 | 8 | 如果环境里没有 `npm` 则需要先安装 `npm` 9 | 10 | 2. 进入本项目根目录 11 | 12 | 3. 安装 gitbook: 13 | 14 | ```bash 15 | gitbook install 16 | ``` 17 | 18 | 这一步通常会出现类似如下报错: 19 | ```bash 20 | Installing GitBook 3.2.3 21 | /usr/local/lib/node_modules/gitbook-cli/node_modules/npm/node_modules/graceful-fs/polyfills.js:287 22 | if (cb) cb.apply(this, arguments) 23 | ^ 24 | 25 | TypeError: cb.apply is not a function 26 | at /usr/local/lib/node_modules/gitbook-cli/node_modules/npm/node_modules/graceful-fs/polyfills.js:287:18 27 | at FSReqCallback.oncomplete (fs.js:169:5) 28 | ``` 29 | 30 | 此时进入该代码文件,将62至64行的如下代码删去,然后重新运行 `gitbook install` 即可: 31 | ```js 32 | fs.stat = statFix(fs.stat) 33 | fs.fstat = statFix(fs.fstat) 34 | fs.lstat = statFix(fs.lstat) 35 | ``` 36 | 37 | 4. 在本地构建文档: 38 | 39 | ```bash 40 | gitbook build 41 | ``` 42 | 43 | 完成后可以在 `_book/index` 访问该文档,**但无法点击文档内项目进行跳转**。 44 | 45 | 此时可运行如下命令: 46 | ```bash 47 | sed -i "s/if(m)for(n/if(false)for(n/g" _book/gitbook/theme.js 48 | ``` 49 | 50 | 再次打开文档后就可以跳转了。 51 | 52 | ## 常见问题 53 | 54 | 1. Q: `gitbook build`生成的_book下html在左侧栏无法跳转菜单 55 | 56 | A: 57 | 58 | 在导出的文件夹目录下找到gitbook->theme.js文件.找到下面的代码搜索 if(m)for(n.handler&& 59 | 60 | 将if(m)改成if(false) 61 | 62 | 再次打开_book下的index.html页面,确认能够跳转页面。 63 | 64 | ### 如果有其他奇奇怪怪的问题 65 | 66 | 由于 [gitbook-cli](https://github.com/GitbookIO/gitbook) 的开发及维护已经废止,如果有其他奇奇怪怪的问题的话,可以尝试使用修复了 gitbook-cli 的最后版本中的一些 bug 的 [gitbook-ng](https://www.npmjs.com/package/@gitbook-ng/gitbook/v/3.3.6)。 67 | -------------------------------------------------------------------------------- /readme_for_ta.md: -------------------------------------------------------------------------------- 1 | # 2023 对文档的改动 2 | 3 | 将往年文档中的手写 parser 等文档删除,整理了文档结构,可以看一下对应commit的记录。 4 | 5 | # 2024 对文档的改动 6 | 7 | - 删除了 step14 SSA,现在选做二只包括 step13 寄存器分配算法这一个选择。 8 | - 添加了大实验的文档。 -------------------------------------------------------------------------------- /styles/pdf.css: -------------------------------------------------------------------------------- 1 | website.css -------------------------------------------------------------------------------- /styles/website.css: -------------------------------------------------------------------------------- 1 | .SpecRuleStart { color: #af0087; } 2 | .SpecRuleIndicator { color: #af0087; } 3 | .SpecOperator { color: red; } 4 | .Normal { background-color: #ffffff; padding-bottom: 1px; } 5 | .SpecToken { color: #af5f00; } 6 | .SpecRule { color: #8080ff; } 7 | .changed { background: lightgrey; } 8 | --------------------------------------------------------------------------------