diff --git a/README.md b/README.md index c112eb528..9f942afd2 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # MiniDecaf 编译实验 -> 助教在这里有一些想说的:实验文档看起来会有一些长,是因为编译器本身就是一个庞大的系统,我们希望提供尽可能全面的内容来帮助大家理解框架的构成,忽略框架和编译本身没有关系的知识。请大家认真阅读文档,并且尽可能按照文档去动手试一试,而不是直接开始动手写作业。 +> 实验手册指北:实验文档看起来会有一些长,是因为编译器本身就是一个庞大的系统,我们希望提供尽可能全面的内容来帮助大家理解框架的构成,忽略框架和编译本身没有关系的知识。请大家认真阅读文档,并且尽可能按照文档去动手试一试,而不是直接开始动手写作业。 ## 实验概述 MiniDecaf [^1] 是一个 C 的子集,去掉了`include/define`等预处理指令,多文件编译支持,以及结构体/指针等语言特性。 本学期的编译实验要求同学们通过多次“思考-实现-重新设计”的过程,一步步实现从简单到复杂的 MiniDecaf 语言的完整编译器,能够把 MiniDecaf 代码编译到 RISC-V 汇编代码。进而深入理解编译原理和相关概念,同时具备基本的编译技术开发能力,能够解决编译技术问题。MiniDecaf 编译实验分为多个 stage,每个 stage 包含多个 step,共包含 12 个 step。**每个 step 大家都会完成一个可以运行的编译器**,把不同的 MiniDecaf 程序代码编译成 RISC-V 汇编代码,可以在 QEMU/SPIKE 硬件模拟器上执行。随着实验内容一步步推进,MiniDecaf 语言将从简单变得复杂。每个步骤都会增加部分语言特性,以及支持相关语言特性的编译器结构或程序(如符号表、数据流分析方法、寄存器分配方法等)。下面是采用 MiniDecaf 语言实现的快速排序程序,与 C 语言相同。 @@ -24,15 +24,9 @@ int qsort(int a[], int l, int r) { 2024 年秋季学期基本沿用了 2023 年秋季学期《编译原理》课程的语法规范。为了贴合课程教学内容,提升训练效果,课程组设计了比较完善的编译器框架,包括词法分析、语法分析、语义分析、中间代码生成、数据流分析、寄存器分配、目标平台汇编代码生成等步骤。每个 step 同学们都会面对一个完整的编译器流程,但不必担心,实验开始的几个 step 涉及的编译器框架知识都比较初级,随着课程实验的深入,将会循序渐进地引入各个编译器功能模块,并通过文档对相关技术进行分析介绍,便于同学们实现相关编译功能模块。 - - ## 实验起点和基本要求 -本次实验一共设置 13 个步骤(其中 step0 为实验框架熟悉,不需要修改框架代码)。后续的 step1-13 我们将由易到难完成 MiniDecaf 语言的所有特性,由于编译器的边界情况很多,你**只需通过我们提供的正例与负例即可**。 +本次实验一共设置 13 个步骤(其中 step 0 和 step 1 为实验框架熟悉,不需要修改框架代码)。后续的 step 2-13 我们将由易到难完成 MiniDecaf 语言的所有特性,由于编译器的边界情况很多,你**只需通过我们提供的正例与负例即可**。 我们以 stage 组织实验,各个 stage 组织如下: diff --git a/docs/ref/intro.md b/docs/ref/intro.md deleted file mode 100644 index 30f64a469..000000000 --- a/docs/ref/intro.md +++ /dev/null @@ -1,28 +0,0 @@ -# 说明 - -我们提供了一系列完整的 step1~step12 的参考实现,分别使用了不同的编程语言或词法语法分析工具,如下所示: -> 参考实现仅供参考,不是标准答案! - -> git.tsinghua 上有[镜像](https://git.tsinghua.edu.cn/decaf-lang/minidecaf) - -## [Python-ANTLR](./python-dzy.md) -* 地址 https://github.com/decaf-lang/minidecaf/tree/md-dzy -* clone 命令:`git clone https://github.com/decaf-lang/minidecaf.git -b md-dzy` -* 演示网址:https://hoblovski.github.io/minidecaf-web/ - - 请耐心加载,可能要一分钟,加载好以后第一次编译要十秒,之后就快了 - - 因为要动态把 py 翻译成 js 然后执行,三重的缓慢 - -## Rust-lalr1 -* 地址 https://github.com/decaf-lang/minidecaf/tree/mashplant -* clone 命令:`git clone https://github.com/decaf-lang/minidecaf.git -b mashplant` -* 演示网址:https://mashplant.online/minidecaf-frontend/ - - 除了加载可能因为网络原因稍慢,之后的运行都非常快。原理是Rust编译到[WASM](http://webassembly.org.cn/)在网页中执行,感兴趣的同学可以自行了解 - -## [TypeScript-ANTLR](./typescript-jyk.md) -* 地址 https://github.com/equation314/minidecaf -* clone 命令:`git clone https://github.com/equation314/minidecaf.git --recursive` -* 演示网址:https://equation314.github.io/minidecaf - -## [Java-ANTLR](./java-xxy.md) -* 地址 https://github.com/decaf-lang/minidecaf/tree/md-xxy -* clone 命令:`git clone https://github.com/decaf-lang/minidecaf.git -b md-xxy` diff --git a/docs/ref/java-xxy.md b/docs/ref/java-xxy.md deleted file mode 100644 index 4715bb78e..000000000 --- a/docs/ref/java-xxy.md +++ /dev/null @@ -1,77 +0,0 @@ -# Java-ANTLR -这个文档用来介绍 Java-ANTLR,以及解释它的实现和指导书的不同。 - -## 概述 -本参考实现基于 Java (JDK 1.4)语言和 [Gradle](https://gradle.org/) 项目构建工具,使用 [ANTLR](https://www.antlr.org/) 工具进行词法语法分析。 - -本参考实现具有以下特点: -- **基于ANTLR的词法语法分析**,并且使用 Gradle 的 ANTLR 插件,十分简单易用; -- **单遍遍历**,代码比较短,但逻辑比较糅杂。 - -## 环境配置 - -可见于[这里](https://github.com/decaf-lang/minidecaf/blob/md-xxy/README.md)。 - -## 代码结构 - -下面是最终完整实现的 minidecaf 编译器中的代码结构 - -``` -src/main -├── antlr/minidecaf/ -│ └── MiniDecaf.g4 ANTLR 语法文件 -├── java/minidecaf/ -│ ├── Main.java 主体驱动部分 -│ ├── MainVisitor.java 主体编译逻辑 -│ ├── Type.java 基本类型,包括“无类型”、整型和指针 -│ ├── FunType.java 函数类型 -│ └── Symbol.java 符号 -├── build.gradle gradle 构建脚本 -└── settings.gradle gradle 配置文件 -``` - -## 与总实验指导的区别 - -本参考框架在分析树上单遍遍历直接生成汇编代码,所以没有语义检查、生成 IR 等中间阶段,所以与总实验指导差别很大,建议大家优先阅读参考代码相邻版本之间的 diff 和参考代码中的注释。 - -### 局部变量(step 5) - -由于我们只有单遍遍历,在遍历函数体之前不可能知道函数体中局部变量的数量。 - -为了在函数的序言部分为局部变量开出足够大的栈空间,我们需要使用 **代码回填** 技术。也就是在遍历完函数体,计算出局部变量的数量 x 和其所占的内存空间字节数 y = 4x,再将 `addi sp, sp, -y` 插入到函数序言中。 - -### 调用约定(step 9) - -本参考实现遵循了 step 9 中描述的 GCC 调用约定。 - -### 全局变量(step 10) - -为方便理解,我们会将全局变量放在一个单独的符号表里。 - -“全局变量的初始值只能是整数字面量”这一点我们不作为语义规范,而是直接将其在语法层面避免。我们会将 `declaration` 分为 `localDecl` 和 `globalDecl` 两种不同的非终结符,然后约定 `globalDecl` 的初始化表达式只能是一个整数字面量。 - -所以,本参考实现的语法相较于语法规范会在以下相应部分有所修改。 - -``` -program: - (function | globalDecl)* - -block_item: - statement - | localDecl - -globalDecl: - type Identifier ('=' Integer)? ';' - -statement: - 'for' '(' localDecl expression? ';' expression? ')' statement - -localDecl: - type Identifier ('=' expr)? ';' -``` - -### 指针(step 11) - -由于我们只有单遍遍历,无法采用主指导书中对左值的处理方式(无法知道一个地方是否 **需要** 左值)。所以我们这里会使用另一种方式: - -对于左值,我们会优先在栈中保留其地址,在需要用到其值的时候才将其值从地址中读出(将左值转换为右值)。 diff --git a/docs/ref/pics/sf-dzy.drawio b/docs/ref/pics/sf-dzy.drawio deleted file mode 100644 index f6fa9f381..000000000 --- a/docs/ref/pics/sf-dzy.drawio +++ /dev/null @@ -1 +0,0 @@ -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 \ No newline at end of file diff --git a/docs/ref/pics/sf-dzy.svg b/docs/ref/pics/sf-dzy.svg deleted file mode 100644 index 55196fbce..000000000 --- a/docs/ref/pics/sf-dzy.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
int bar(void) {
    int a; scanf("%d", &a);
    int b; scanf("%d", &b);
    int c = 0;
    c = b + a;
    return c; }
int foo(){return bar();}
int main(){return foo();}
int bar(void) {...
foo 的栈帧
foo 的栈帧
main 的栈帧
main 的栈帧

bar 的栈帧

bar 的栈帧
低地址
低地址
高地址
高地址
fp
fp
c (==0)
c (==0)
局部变量
局部变量
b (==12)
b (==12)
a (==24)
a (==24)
old fp
old fp
return address
return address
12
12
24
24
表达式运算栈
表达式运算栈
……
读取 b 并放到栈顶
读取 a 并放到栈顶
add
将栈顶保存到 c
语句完成
……
……读取 b 并放到栈顶...
1.
1.
sp
sp
fp+4
fp+4
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/ref/pics/ts_stack_frame.svg b/docs/ref/pics/ts_stack_frame.svg deleted file mode 100644 index 0e450c359..000000000 --- a/docs/ref/pics/ts_stack_frame.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
被调用者栈帧
被调用者栈帧
调用者栈帧
调用者栈帧
fp - 8
fp - 8
fp - 4
fp - 4
fp
fp
sp
sp
栈增长方向
栈增长方向
stack item 1
stack item 1
saved fp
saved fp
...
...
local var 1
local var 1
local var 0
local var 0
...
...
arg 9
arg 9
arg 8
arg 8
saved ra
saved ra
saved a0
saved a0
...
...
saved a7
saved a7
stack item 0
stack item 0
保存的参数寄存器
保存的参数寄存器
表达式运算栈
表达式运算栈
参数
参数
保存的寄存器
保存的寄存器
局部变量
局部变量
高地址
高地址
低地址
低地址
Viewer does not support full SVG 1.1
\ No newline at end of file diff --git a/docs/ref/pics/ts_workflow.svg b/docs/ref/pics/ts_workflow.svg deleted file mode 100644 index 127984b51..000000000 --- a/docs/ref/pics/ts_workflow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - -
词法语法分析
词法语法分析
语义检查
语义检查
中间代码生成
中间代码生成
解释器
解释器
目标代码生成
目标代码生成
GCC
GCC
QEMU
QEMU
MiniDecaf 源程序
MiniDecaf 源程序
ANTLR 分析树
ANTLR 分析树
中间表示
中间表示
RISC-V 汇编
RISC-V 汇编
可执行程序
可执行程序
执行结果
执行结果
参考实现的
编译器
参考实现的 编译器
Viewer does not support full SVG 1.1
diff --git a/docs/ref/python-dzy.md b/docs/ref/python-dzy.md deleted file mode 100644 index 580beaaef..000000000 --- a/docs/ref/python-dzy.md +++ /dev/null @@ -1,137 +0,0 @@ -# Python-ANTLR -这个文档用来介绍 Python-ANTLR,以及解释它的实现和指导书的不同。 - -# 概述 -Python-ANTLR 参考实现使用 python 语言(python >= 3.6),使用 [ANTLR](https://www.antlr.org/) 工具进行词法语法分析。 -> 网页版使用 brython 动态翻译 python 到 javascript 执行。 - -如果你要看 Python-ANTLR,至少你要会 python 3,要会写 python 的 `class`,看得懂 `[a+1 for a in l]` 等等。 - -Python-ANTLR 的特点,以及一些注意事项是 -* 用 **ANTLR** 做语法词法分析,所以词法语法分析很简单。 -* **真·多遍**,词法语法分析、名称解析(step7)、类型检查(step12)、IR 生成、汇编生成几个过程都是独立的。 -* 前面的几个 step 是 64 位的,在 commit: 49aecac 中修正, - 并且前面 step 也有 bug 到后面才被修正。 - > 因此代码 **仅供参考** ,如果你直接切到前面的 commit 是不能通过测试的。 -* 每个 step 都是一个或多个 commit,可以一个一个 commit 来看每个步骤到底改了什么 - -commit 大致分为几类,根据 commit message 分为: - -| commit message | 这个 commit 做了啥 | -| --- | --- | -| `BUGFIX ...` / `fix ...` | 修正以前实现的 bug | -| `step *. s* ....` | 完成某 step(后面 s1..s6 是六个大步骤,step 是小步骤) | -| `... refactor ...` | 代码重构 | - -# 实验框架 -Python-ANTLR 大致思路和实验指导书相同,但有一些小区别。 - -## 代码结构 -下面是 12 个 step 完全做完以后 minidecaf 中,各个主要文件和目录的作用。 - -``` -minidecaf -├── __init__.py -├── __main__.py -├── main.py main。顶层逻辑,minidecaf 从这里开始执行 -├── utils.py -│ -├── asm 编译器后端:IR 到汇编 -│   ├── command.py 汇编中可能出现哪几种元素 -│   ├── __init__.py 后端通用框架 -│   └── riscv.py RISC-V 对于通用框架的实现 -│ -├── frontend 编译器前端:词法语法分析、名称解析、类型检查 -│   ├── __init__.py 把 namer/irgen/typer 几个阶段包装一下给 main 用 -│   ├── irgen.py IR 生成阶段 -│   ├── namer.py 名称解析阶段 -│   ├── typer.py 类型检查阶段 -│   └── types.py 类型定义和类型规则(给 typer 用的) -│ -├── generated ANTLR 生成的代码放在这里面 -│   └── __init__.py -│ -├── ir IR 的定义 -│   ├── __init__.py 定义 IR 的函数/全局变量/程序 -│   ├── instr.py 各 IR 指令 -│   └── visitor.py 后端实现为一个 IR visitor -│ -├── CommonLex.g4 词法定义,就是 ../specs/CommonLex.g4 -├── MiniDecaf.g4 语法定义,就是 ../specs/s6.g4 -└── requirements.txt minidecaf 作为 python 包的依赖 -``` - -## IR 的区别 -我们的 IR 和指导书一样,但有些名字有些不同 - -| Step | 指导书的 IR | Python-ANTLR | 备注 | -| --- | --- | --- | --- | -| 1 | push | Const | | -| 1 | ret | Ret | | -| 2 | neg, not, lnot | Unary | | -| 3,4 | add, sub, mul, div, rem, eq, ne, lt, le, gt, ge, land, lor | Binary | | -| / | 实验指导书中没有 | Comment | | -| 5 | pop | Pop | | -| 5 | load | Load | | -| 5 | store | Store | | -| 5 | frameaddr | FrameSlot | 设参数是 `k`,则 `frameslot k` 最终在 `k(fp)`,而 `frameaddr k` 在 `-12-4*k(fp)` | -| 6 | label | Label | | -| 6 | br, beqz, bnez | Branch | | -| 9 | globaladdr | GlobalSymbol | | -| 9 | call | Call | | - -并且栈帧也略有不同,fp 的位置有变化。 -所以局部变量的位置不是 `-12(fp)`、`-16(fp)` 而是 `-4(fp)`、`-8(fp)` 以此类推。 - -![](./pics/sf-dzy.svg) - -## step5 -不是计算 `FRAMESIZE` 然后在 prologue 直接 `addi sp, sp, -FRAMESIZE`,而是每次遇到声明再分配 4 个字节空间,离开声明所在作用域时在释放其空间(待 step7 引入作用域后)。 - -所以如下代码 -``` -int main() { - int a=144; - { int a=155; } - return a; -} -``` - -会翻译为如下 IR: -``` - const 144 # 栈大小加 4、给 a 分配空间;正好 4 就放到 a 的内存里作为初值 - const 155 # 给第二个 a 分配空间和设置初值 - pop # 离开第二个 a 的作用域,释放第二个 a 所占的空间 - frameslot -4 # 这三条都是读取 a 的 - load - ret - pop # 离开第一个 a 的作用域、释放空间(上面有 ret,所以这个 pop 永远不会被执行) -``` - -## step7 -名称解析要求我们确定源代码中一个变量名到底是引用哪个变量。 - -对于每个函数,其名称解析结果放在它的 `FuncNameInfo` 中,作为一个 dict `_v`。 -* 变量名用它的 AST 节点表示,例如 `ctx.Ident()`,其中 `ctx` 可能是 `AtomIdentContext`、`DeclContext` 等。 -* 变量用 `Variable` 表示,`Variable.ident` 是变量名,`Variable.id` 是一个计数器用来区分同名变量。 - -然后把各个函数的 `FuncNameInfo` 一起放到 `NameInfo` 里。 - -## step9 -参数处理不使用指导书上说的办法: -prologue 里面要把参数从 caller 的栈帧复制到 callee 的栈帧,然后当成普通 `decl` 处理。 - -调用约定就是指导书上说的非标准的调用约定(参数从右往左压栈)。 - -## step12 -左值分析放到 typer 里面做了,在 `class Locator` 里面。 -它计算左值的地址,然后返回值是一个列表,包含 IR 指令和 AST 结点。 -例如 `int a[6];` 那么 `a[2+3]` 的左值地址就是 -`[ a 的 AST 结点, 2+3 的 AST 结点, const 4, mul, add ]`。 - -类型规则用一个函数表示,参数是操作数类型,函数要加 `@TypeRule` 修饰。 -如果操作数类型满足规则的要求,那么返回结果类型,否则返回一个字符串表示报错消息。 - -typer 遍历 AST 时记录表达式类型(方法类似[Visitor 文档最后一部分](../lab1/visitor.md)),类型保存到 `TypeInfo` 里。 -注意 `FuncTypeInfo` 只函数本身的参数类型/个数和返回值类型,它和 `TypeInfo` 的关系与 `FuncNameInfo` 和 `NameInfo` 的关系不同。 - diff --git a/docs/ref/typescript-jyk.md b/docs/ref/typescript-jyk.md deleted file mode 100644 index e8651ee57..000000000 --- a/docs/ref/typescript-jyk.md +++ /dev/null @@ -1,329 +0,0 @@ -# TypeScript-ANTLR - -* 地址 https://github.com/equation314/minidecaf -* 演示网址:https://equation314.github.io/minidecaf - -## 概述 - -本参考实现基于 [TypeScript](https://www.typescriptlang.org/) 语言,使用 [ANTLR](https://www.antlr.org/) 工具进行语法分析。可以直接转换为 JavaScript,原生支持在浏览器上运行。 - -本参考实现具有以下特点: - -* **基于 ANTLR 的词法语法分析**。使用了 ANTLR 工具进行词法和语法分析,详见 [ANTLR 使用](../lab1/antlr.md)。 -* **网页版编译器**。通过将 TypeScript 翻译到 JavaScript,可直接在浏览器中运行 MiniDecaf 编译器,生成 RISC-V 汇编。 -* **在浏览器上运行源程序**。本参考代码基于对生成的中间表示的模拟执行,实现了一个解释器,能够在网页上直接运行源程序,并得到执行结果(`main` 函数返回值),无需再用 QEMU 等模拟器运行。 - -此外,与其他参考代码一样,本参考代码完成每个 step 的过程,都被分成了一个或多个 [git commit](https://github.com/equation314/minidecaf/commits/master),你可以使用 `git diff` 得到相邻两次 commit 的差异,来明确每一步需要完成哪些工作。每一个 step 的最后一个 commit 都被打上了 [tag](https://github.com/equation314/minidecaf/tags),并都能通过部署在 Github Actions 上的[自动测试](https://github.com/equation314/minidecaf/actions?query=workflow%3A%22Build+%26+Test%22)。 - -**特别注意:实现网页版编译器和解释器不是本实验的必做内容。** - -### TypeScript 是什么? - -TypeScript 是 JavaScript 的超集(类似于 C++ 是 C 的超集),顾名思义就是加了类型的 JavaScript。除了类型系统外,还增加了接口、枚举、泛型等众多新特性,比 JavaScript 更加面向对象,更加容易编写,更能发现潜在的错误,更适合开发大型应用。 - -TypeScript 可以被直接翻译到 JavaScript,因此不仅能使用 [Node.js](https://nodejs.org/en/) 在命令行里运行,还支持在浏览器里运行。本参考代码就是通过这种方式,先用更加友好的 TypeScript 进行开发,再转成 JavaScript,实现了能在浏览器里运行的编译器。 - -> 由于 TypeScript 对 JavaScript 的兼容性,你也可以直接用 JavaScript 语言进行开发。 - -### 要不要选择用 TypeScript 写编译器? - -如果你已经会了 TypeScript 语言,或对 Web 开发感兴趣,或想要一个自己亲手制作的炫酷的网页版编译器,强烈建议使用 TypeScript 语言进行实验。如果你只是会 JavaScript,那么上手 TypeScript 是很容易的,当然你也可以直接用 JavaScript 进行开发。如果两者都不熟悉也没关系,作为脚本语言,它们都非常简单易学。 - -关于 TypeScript 的入门教程与文档,详见: - -* 官方入门教程:https://www.typescriptlang.org/docs/handbook/typescript-tooling-in-5-minutes.html -* 官方手册:https://www.typescriptlang.org/docs/handbook/intro.html -* 中文版教程与手册:https://www.tslang.cn/docs/home.html - -## 环境配置 - -下面给出了此版本参考代码的环境配置、构建与运行、以及测试的方法。 - -### 安装 Node.js - -去[官网](https://nodejs.org/en/download/)下载并安装 Node.js(同时包含了包管理器 npm),建议使用 12 以上的版本。 - -### 安装依赖 - -进入参考代码目录,先运行以下命令一键安装依赖: - -```bash -npm install -``` - -运行完后当前目录中会多出一个 `node_modules` 文件夹,里边包含了开发所需的各种所需的软件包和运行库,例如 TypeScript 编译器 `tsc`、ANTLR 工具等等。 - -> 你无需再像[step1:词法语法分析工具](../lab1/part2.md)那里一样下载 ANTLR 的 JAR 包。 - -### 命令行运行 - -```bash -npm run grammar # 生成 ANTLR 语法分析器 -npm run build # 将 TypeScript 编译成 JavaScript -npm run cli test/test.c -- -s # 使用 node 运行生成的 JS 代码,编译 test/test.c 并生成汇编码 -``` - -即可看到对[测试文件](https://github.com/equation314/minidecaf/blob/master/test/test.c)编译后生成的 RISC-V 汇编码。上面第三条命令后面的是编译选项,`-s` 表示生成汇编码,不加 `-s` 就会直接运行并输出 `main` 函数的返回值。运行 `npm run cli -- -h` 查看更多编译选项。 - -### 测试 - -测试单个文件(即 [test/test.c](https://github.com/equation314/minidecaf/blob/master/test/test.c)),得到它的运行结果(`main` 函数返回值): - -```bash -npm run test # 作为解释器运行 -# 或 `npm run test-codegen`,会先生成 RISC-V 汇编码,再在模拟器(QEMU 或 Spike)中运行。 -``` - -运行我们提供的所有[测例](https://github.com/decaf-lang/minidecaf-tests): - -```bash -npm run test-all -- -n 12 # 作为解释器运行 -# 或 `npm run test-all -- -s -n 12`,会先生成 RISC-V 汇编码,再在模拟器(QEMU 或 Spike)中运行。 -``` - -这里 `-n ` 表示只运行 step1 到 stepN 的测例。 - -> 请确保你在 clone 仓库时加了 `--recursive` 选项,将[测例仓库](https://github.com/decaf-lang/minidecaf-tests)也一起 clone 了下来。 -> -> 如果加了 `-s` 选项,会自动调用 [minidecaf-tests/check.sh](https://github.com/decaf-lang/minidecaf-tests/blob/master/check.sh) 运行测试。但该脚本不支持测试解释器,只好写在 [test/test_all.sh](https://github.com/equation314/minidecaf/blob/master/test/test_all.sh) 中。 - -### 【可选】网页版编译器 - -```bash -npm run build-web # 将 TS 编译成 JS,然后将所有 JS 文件打包以便在网页上调用 -npm run serve # 启动简易的 HTTP 静态服务器 -``` - -在浏览器中打开网址 http://127.0.0.1:8080 即可看到,效果与 https://equation314.github.io/minidecaf 一样。在左上角输入 MiniDecaf 源代码,点击“Run”即可编译,左下角会得到运行结果,右上角和右下角分别是生成的中间表示与 RISC-V 汇编码。 - -## 实验框架 - -如果你打算使用 TypeScript/JavaScript 语言完成本实验,实现网页版编译器,我们提供了一个[实验框架](https://github.com/equation314/minidecaf/tree/skeleton)。该框架已经帮你完成了 TypeScript 项目配置、网页前端开发、自动测试和部署等与实验无关的内容,你只需集中精力完成实验部分,填入你自己的编译器的实现,即可得到一个你自己的网页版编译器。 - -### 快速入门 - -在 clone 整个仓库后,切换到 `skeleton` 分支,即可开始实验。 - -```bash -git clone https://github.com/equation314/minidecaf.git --recursive -git checkout skeleton -``` - -实验框架已经进行了基本的项目配置,你可直接运行与上一节“环境配置”中类似的命令: - -```bash -npm install # 安装依赖 -npm run build # 将 TS 转 JS -npm run cli test/test.c -- -s # 运行 JS 代码,生成汇编 - -# 测试 -npm run test-codegen # 对 test/test.c 生成汇编,并在模拟器中运行 -npm run test-all -- -s [-n ] # 运行 step1 到 step 的全部测例 - -#【可选】构建网站 -npm run build-web # 将 TS 编译成 JS 并打包 -npm run serve # 启动简易的 HTTP 静态服务器 -``` - -> 框架中没有实现 `npm run grammar` 命令,这与你的语法分析器的实现方式有关。如果你也使用一些工具进行语法分析器的生成,可以在 [package.json](https://github.com/equation314/minidecaf/blob/skeleton/package.json) 中添加类似的命令。例如要安装和使用 TypeScript 版的 ANTLR,详见参考代码或 https://github.com/tunnelvisionlabs/antlr4ts 。 - -运行完后,只会输出一行 `nop`。这是因为[框架中的编译函数](https://github.com/equation314/minidecaf/blob/skeleton/src/minidecaf.ts)就是这么写的: - -```ts -export function compile(input: string, option: CompilerOption): string { - if (option.target === CompilerTarget.Riscv32Asm) { - return "nop"; - } else if (option.target === CompilerTarget.Executed) { - return "0"; - } -} -``` - -> 本实验无需实现解释器,你只需要处理 `option.target === CompilerTarget.Riscv32Asm` 的情况即可。 - -本函数就是你在实验中需要完成的部分。在每一个 step 中,你都需要让该函数返回正确的 RISC-V 汇编。你可以随意增加文件,不过不建议对除 [src/minidecaf.ts](https://github.com/equation314/minidecaf/blob/skeleton/src/minidecaf.ts) 以外的文件做修改。 - -为了让部署在 git.tsinghua.edu.cn 中的 CI 能自动测试你的代码,**请确保对给定源文件生成汇编的命令为以下格式**: - -```bash -npm run cli -- -s -o -``` - -如果你不使用我们提供的框架,请自行在 `package.json` 的 `scripts` 字段中添加 `cli` 字段,并填好运行你的 JS/TS 编译器的命令。此外你的编译器至少需要支持 `-s` 和 `-o` 选项。如果不想自己折腾建议直接使用我们的框架。 - -### 【可选】自动测试与部署 - -如果你使用 Github 进行代码托管,可使用 [Github Actions](https://docs.github.com/en/actions) 搭建 CI(continuous integration),进行自动测试与网站的自动部署。我们已经提供了 [workflow 文件](https://github.com/equation314/minidecaf/tree/master/.github/workflows),每次 push 任何分支都会使用 [test.yml](https://github.com/equation314/minidecaf/blob/master/.github/workflows/test.yml) 中的配置,自动构建并跑我们的测试用例;每次 push master 都会创建 GitHub Pages,部署网页版编译器。 - -[test.yml](https://github.com/equation314/minidecaf/blob/master/.github/workflows/test.yml) 中也包含了如何在一个干净的系统中配置实验环境的命令,如安装 RISC-V 工具链、安装 QEMU 等,可以作为配置环境时的参考。 - -## 与实验总指导的差异 - -本节列出了本参考实现与实验总指导的几处主要不同,能够帮你更好地理解这份参考代码。参考代码中也提供了详细的注释帮助你理解。你在做实验时应该主要关注实验总指导,无需和这里的实现一样。另外本节内容涉及多个 step 中的细节,建议根据你目前所做的 step 选择性查阅相关内容。 - -### 整体架构 - -参考代码中编译器的整体流程如下: - -![参考实现的整体编译流程](pics/ts_workflow.svg) - -1. 源代码经过 ANTLR 词法和语法分析器,生成 ANTLR 分析树; -2. 使用 ANTLR visitor 模式对分析树进行名称解析、类型检查等语义检查,并在节点上标记一些属性; -3. 使用 ANTLR visitor 模式对分析树生成中间表示; -4. 将中间表示转换为 RISC-V 汇编,之后用 GCC 生成可执行文件,并在 QEMU/Spike 上运行; -5. 【可选】用解释器模拟中间代码的执行,直接得到结果。 - -以下是参考代码的目录结构: - -``` -src/ -├── grammar/ # 语法 -│ ├── Lexer.g4 # ANTLR 词法规则 -│ └── MiniDecaf.g4 # ANTLR 语法规则 -├── target/ # 编译目标 -│ ├── executor.ts # IR 解释执行器 -│ └── riscv.ts # IR 到 RISC-V 的代码生成 -├── visitor/ # ANTLR visitors -│ ├── irgen.ts # IR 生成器 -│ └── semantic.ts # 语义检查 -├── cli.ts # 命令行工具 -├── error.ts # 定义了各类错误 -├── ir.ts # 中间表示 -├── minidecaf.ts # 编译器主入口 -├── scope.ts # 作用域 -└── type.ts # 类型系统 -``` - -### 语法树(所有 step) - -本参考实现没有真正构建出抽象语法树,而是直接使用了 ANTLR 自动生成的分析树。 - -在第一次遍历分析树时(详见 [src/target/semantic.ts](https://github.com/equation314/minidecaf/blob/master/src/visitor/semantic.ts)),会给分析树增加一些额外的属性,以便之后的分析。使用 JavaScript 的语法可以方便地给任何 object 增加属性,例如: - -```ts -visitType(ctx: MiniDecafParser.TypeContext): Result { - if (ctx.Int()) { - ctx["ty"] = BaseType.Int; - } else { - ctx["ty"] = new PointerType(ctx.type().accept(this)["ty"]); - } - return ctx; -} -``` - -其中属性 `ty` 表示节点的类型。其他重要的属性还有: - -| 属性名 | 具有该属性的节点 | 含义 | -|-------|---------------|------| -| `ty` | 表达式 | 类型 | -| `lvalue` | 表达式 | 是否是左值 | -| `paramCount` | 函数 |参数个数 | -| `localVarSize` | 函数 | 局部变量所占内存大小 | -| `variable` | 标识符 | 对应的变量(`Variable` 类) | -| `loop` | break、continue 语句 | 对应的循环语句节点 | - -### 中间表示(所有 step) - -为了方便直接在浏览器上执行源程序并得到结果,本参考代码也使用了中间表示,并实现了对中间代码的模拟执行。不过与 step1 中讲的 [IR 简明介绍](../lab1/ir.md)这一节有所不同,这里的 IR 不只是一个简单的栈结构,还包含了两个寄存器。 - -> 如果你不打算实现解释器,可不使用中间代码,直接从 AST 生成汇编。 - -在 [IR 简明介绍](../lab1/ir.md)和之后的 step2、step3 中,我们介绍了如何使用基于栈的 IR 来表示一元和二元运算。例如,要计算表达式 `1 + (-2)`,IR 如下: - -```python -PUSH 1 # 把 1 压入栈顶 -PUSH 2 # 把 2 压入栈顶 -NEG # 从栈顶弹出一个元素,对其取相反数,再压入栈顶 -ADD # 从栈顶弹出两个元素,计算它们的和,再压入栈顶 -``` - -直接转换成 RISC-V 汇编码,将会是: - -```asm -# PUSH 1 -addi sp, sp, -4 -li t1, 1 -sw t1, 0(sp) -# PUSH 2 -addi sp, sp, -4 -li t1, 2 -sw t1, 0(sp) -# NEG -lw t1 0(sp) -neg t1, t1 -sw t1, 0(sp) -# ADD -lw t1, 4(sp) -lw t2, 0(sp) -add t1, t1, t2 -addi sp, sp, 4 -sw t1, 0(sp) -``` - -可以发现,这种 IR 的一个缺点是压栈、弹栈操作太多。最终生成的汇编代码有 16 条指令,其中真正用于计算的指令才 2 条(`neg` 和 `add`),而剩下的 14 条都是与计算结果无关的栈操作,运行效率可想而知。 - -本参考实现中对以上 IR 做了一个简单优化,即引入了两个寄存器 `r0` 和 `r1`,上一步的计算结果默认存在 `r0` 中而不是栈顶。这样尽可能使用寄存器而不是栈来保存中间结果,使得 IR 更加接近于机器代码,执行效率更高。例如,使用该 IR 计算表达式 `1 + (-2)` 如下: - -```python -IMM 1, r0 # r0 = 1 -PUSH r0 # 把 r0 压入栈顶 -IMM 2, r0 # r0 = 2 -NEG r0 # r0 = -r0 -POP r1 # 从栈顶弹出一个元素,存到 r1 -ADD r1, r0 # r0 = r1 + r0 -``` - -转换成 RISC-V 汇编码如下: - -```asm -# IMM 1, r0 -li t0, 1 -# PUSH r0 -addi sp, sp, -4 -sw t0, 0(sp) -# IMM 2, r0 -li t0, 2 -# NEG r0 -neg t0, t0 -# POP r1 -lw t1, 0(sp) -addi sp, sp, 4 -# ADD r1, r0 -addi t0, t1, t0 -``` - -共 8 条指令,比之前的减少了一半。 - -此外,本参考实现中的 IR 指令不隐式包含栈操作,只有 `PUSH`、`POP` 指令可以进行压栈、弹栈(还有 `CALL` 指令在函数调用结束后会从栈中弹出参数个数个元素),而不像之前的 IR 中,执行一条二元运算指令也意味着栈中要减少一个元素, - -完整 IR 的指令表详见 [src/ir.ts](https://github.com/equation314/minidecaf/blob/master/src/ir.ts)。 - -### 调用约定(step9) - -本参考实现遵循了 step9 中描述的 [GCC 的调用约定](../lab9/calling.md): - -> 1. caller-save 和 callee-save 寄存器在 ["Unprivileged Spec"](../lab0/riscv.md) 的 109 页。 -> 2. 返回值(32 位 int)放在 `a0` 寄存器中。 -> 3. 参数(32 位 int)从左到右放在 `a0`、`a1`……`a7` 中。如果还有,则从右往左压栈,第 9 个参数在栈顶。 - -具体地,在 commit [6965523](https://github.com/equation314/minidecaf/commit/69655239e25c901f4b6c10429239ed3f982c303a) 及之前使用的是 step9 中描述的简化版调用约定,在 commit [fd5bccc](https://github.com/equation314/minidecaf/commit/fd5bcccb741258bc0ab746a3034a6aea0acdc973) 更改为了 GCC 的调用约定。如果你也想使用 GCC 的调用约定,可以参考 commit fd5bccc 中的实现过程,否则参考 commit 6965523 即可。 - -实现过程也非常简单粗暴,效率上反而还不如简化版的,只是为了能够调用 GCC 编译的函数。具体做法为:当所有参数从右往左压栈后,从栈中弹出至多 8 个参数,分别存到 `a0`、`a1`……`a7` 中;由于参数寄存器属于临时寄存器,会在嵌套函数调用中被破坏,需要被保存,就在 callee 的 prologue 阶段再将它们保存到 callee 的栈帧上。 - -> 参数寄存器和局部变量如何在栈帧中布局不是调用约定的内容,可以自己任意定义,不需要遵循 GCC 的。 - -为了方便理解,下图给出了本参考实现中的栈帧布局: - -![栈帧布局](pics/ts_stack_frame.svg) - -### 【可选】解释器(所有 step) - -本小节内容是可选的,如果想实现在浏览器中运行源程序,可阅读本小节以供参考。 - -本参考代码实现了对中间代码的模拟执行(详见 [src/target/executor.ts](https://github.com/equation314/minidecaf/blob/master/src/target/executor.ts))。与 IR 的定义一样,解释器实现了对 IR 栈和两个寄存器的模拟。不过为了实现全局变量,还需有另一块内存来放置全局变量。为了方便 step12 实现的取地址操作,参考代码中把这两块内存进行了合并,并使用一个地址 `STACK_OFFSET` 加以区分,该地址以下是栈空间,该地址以上是全局数据的空间。因此还需要有个 `sp` 寄存器来表示栈顶地址。 - -此外,与真实机器以字节为单位的内存不同,模拟的内存就是一个 JavaScript 的数组,其中的元素可以是任意类型,参考代码中使用它来存放数据、地址、函数名等各种东西。而内存地址还是与真实机器的一样 4 个字节对齐,因此在通过地址访问内存数组中的元素时,需要把地址除以 4。 - -参考代码中的 [`Ir` 类](https://github.com/equation314/minidecaf/blob/master/src/ir.ts#L156)由一堆 `IrFunc` 和 `IrGlobalData` 组成,分别表示一个函数与一个全局变量。而一个 `IrFunc` 又由一堆 `IrInstr` 组成,即 IR 的指令。在模拟 IR 执行的过程中,有两个全局的数据 `currentFunc` 和 `pc`,分别表示当前所在的 `IrFunc` 和当前所处理的是其中的哪一条 IR 指令。一般情况下,每遇到一条指令,就会让 `pc` 加 1;如果是跳转指令,就让 `pc` 设为要跳转的标签的位置;如果是 `CALL` 指令,就更新 `currentFunc` 为被调用者,更新 `pc` 为 0。 - -对于函数调用,需要模拟出栈帧的结构,因此需要有一个栈帧寄存器 `fp`,之后对局部变量或参数的引用都是基于 `fp` 的。和生成目标代码一样,一条 IR 指令可能需要完成多步操作。例如 `CALL` 指令需要向栈中压入旧的 `currentFunc`、`pc` 和 `fp`,并更新它们;`RET` 指令需要从栈中恢复它们。对于传参,不需要考虑用寄存器传参,都用栈存放参数即可。 diff --git a/docs/step1/example.md b/docs/step1/example.md index d169993f9..515af3e68 100755 --- a/docs/step1/example.md +++ b/docs/step1/example.md @@ -126,7 +126,7 @@ main: # 主函数入口符号 ret # 返回 ``` -实验框架中关于目标代码生成的文件主要集中 `backend` 文件夹下,step1 中你只需要关注 `backend/riscv` 文件夹中的 `riscvasmemitter.py` 以及 `utils/riscv.py` 即可。具体来说 `backend/asm.py` 中会先调用 `riscvasmemitter.py` 中的 `selectInstr` 方法对每个函数内的 TAC 指令选择相应的 RISC-V 指令,然后会进行数据流分析、寄存器分配等流程,在寄存器分配结束后生成相应的 `NativeInstr` 指令(即所有操作数都已经分配好寄存器的指令),最后通过 `RiscvSubroutineEmitter` 的 `emitEnd` 方法生成每个函数的 RISC-V 汇编。 +实验框架中关于目标代码生成的文件主要集中 `backend` 文件夹下,step1 中你只需要关注 `backend/riscv` 文件夹中的 `riscvasmemitter.py` 以及 `utils/riscv.py` 即可。具体来说 `backend/asm.py` 中会先调用 `riscvasmemitter.py` 中的 `selectInstr` 方法对每个函数内的 TAC 指令选择相应的 RISC-V 指令,然后会进行数据流分析、寄存器分配等流程,在寄存器分配结束后生成真正的汇编指令(即所有操作数都已经分配好寄存器的指令),最后通过 `RiscvSubroutineEmitter` 的 `emitEnd` 方法生成每个函数的 RISC-V 汇编。 ## 思考题 diff --git a/docs/step12/example.md b/docs/step12/example.md index b8b3721b5..f9a3a00df 100644 --- a/docs/step12/example.md +++ b/docs/step12/example.md @@ -67,3 +67,6 @@ a[1] = 2; # 思考题 1. 作为函数参数的数组类型第一维可以为空。事实上,在 C/C++ 中即使标明了第一维的大小,类型检查依然会当作第一维是空的情况处理。如何理解这一设计? + +# 总结 +到目前为止,我们的编译器已经支持了 MiniDecaf 语言的所有特性,包括常量表达式、变量和赋值、作用域和块语句、条件和循环、函数、全局变量和数组。 \ No newline at end of file diff --git a/docs/step13/example.md b/docs/step13/example.md index b73d98f42..1b18b097d 100644 --- a/docs/step13/example.md +++ b/docs/step13/example.md @@ -109,3 +109,8 @@ int f() { - 如果不能合并,那么把 `a` 和 `b` 中间的虚线边改为实线,表示不再考虑二者合并的情况。 上面的说明只是简要介绍了算法的原理,请阅读论文 [TOPLAS'1996: *Iterated Register Coalescing*](https://dl.acm.org/doi/pdf/10.1145/229542.229546) 获取更详细的说明。**别忘了论文末尾的附录有完整的伪代码实现。** + +# 总结 + \ No newline at end of file diff --git a/docs/step4/example.md b/docs/step4/example.md index ad17ac117..047f8c590 100644 --- a/docs/step4/example.md +++ b/docs/step4/example.md @@ -59,4 +59,4 @@ slt t2, t0, t1 # 总结 本步骤中其他运算符的实现逻辑和方法与小于符号类似,可以参考小于符号的实现方法设计实现其他逻辑运算符。 -恭喜你!到目前为止,你已经成功实现了一个基于 MiniDecaf 语言的计算器,可以完成基本的数学运算和逻辑比较运算了,成就感满满!然而,目前你的计算器还只能支持常量计算,这大大降低了计算器的使用体验,因此,在下一个 Stage,我们将一起实现对变量以及分支语句的支持。无论如何,当前的任务已经完成,好好休息一下吧☕️ \ No newline at end of file +恭喜你!到目前为止,你已经成功实现了一个基于 MiniDecaf 语言的计算器,可以完成基本的数学运算和逻辑比较运算了,成就感满满!然而,目前你的计算器还只能支持常量计算,这大大降低了计算器的使用体验,因此,在下一个 Stage,我们将一起实现对变量的支持。无论如何,当前的任务已经完成,好好休息一下吧☕️ \ No newline at end of file diff --git a/docs/step9/example.md b/docs/step9/example.md index b5d1885ff..1b3634521 100644 --- a/docs/step9/example.md +++ b/docs/step9/example.md @@ -115,12 +115,7 @@ func: # end of prologue # start of body - sw a0, 0(sp) - sw a1, 4(sp) - lw t0, 0(sp) - lw t1, 4(sp) - add t2, t0, t1 - mv t0, t2 + add t0, a0, a1 mv a0, t0 j func_exit # end of body @@ -208,13 +203,13 @@ class Asm: 因为寄存器分配过程中我们才能知道有哪些变量需要spill到栈上,分配完所有指令需要的寄存器计算出需要的栈空间大小,因此类似函数开头开辟栈空间的指令`add sp, sp, -56`这样的指令会放在prologue部分。 #### 寄存器分配部分 + 这部分代码主要集中在`backend/reg/bruteregalloc.py`。 首先我们介绍一下`BruteRegAlloc`类中的对象和函数都干了什么: `bindings`用来记录每个临时变量和物理寄存器的对应关系,比如临时变量`T4`如果存放在寄存器`A0`中,那么`bindings`中就会记录 `T0: A0`。你可以通过使用`bind()`, `unbind()`函数来控制绑定关系。 - `accept()`,也就是寄存器分配的起点: ```python def accept(self, graph: CFG, info: SubroutineInfo) -> None: @@ -229,9 +224,7 @@ def accept(self, graph: CFG, info: SubroutineInfo) -> None: ``` 这里对于控制流图(CFG)中的每一个基本块分配了寄存器。 -`localAlloc()`用来给每个基本块指令分配寄存器。我们的实验框架采用了非常简单暴力的寄存器分配:每个基本块前后我们认为所有变量都在栈上,所以你可以在代码中看到`localAlloc()`函数开头我们使用了`self.bindings.clear()`来清除寄存器和栈上变量之间的绑定关系,在分配完每个基本块的寄存器后,我们通过对于所有活跃的寄存器调用`emitStoreToStack`保存到了栈上。 - -因此,在实现Step 9时候,我们虽然使用了寄存器传参,但是我们应该要认为在进入每个基本块的时候,所有变量还是在栈上的。因此我们在生成代码的时候,就应该提前先把变量放到栈上,我们可以通过修改`RiscvSubroutineEmitter`中的`offsets`的来把临时变量和栈上位置对应起来。然后怎么把寄存器放到栈上呢?我们可以看`RiscvSubroutineEmitter.emitEnd`函数,我们会在翻译完所有代码后先把代码保存到buffer里面,先打印一些函数头的信息,然后输出这个buffer中的东西,所以我们就可以在函数头这里把在寄存器中的东西放到栈上。 +`localAlloc()`用来给每个基本块指令分配寄存器。我们的实验框架采用了非常简单暴力的寄存器分配:每个基本块前后我们认为所有变量都在栈上,你如果你觉得这样非常低效并且希望改进一下,你可以在选做的 step 13 中实现一个更加高级的寄存器分配算法。 `allocForLoc()`为每条指令具体分配寄存器。每条指令都有可能要读和写部分临时变量,但是这些临时变量可能不在物理寄存器中,在栈上,因此这个函数为每个需要读、和写的寄存器进行检查是否能在`bindings`中找到绑定关系,如果不在则通过`allocRegFor()`函数来将这些寄存器拿到栈上。 @@ -251,63 +244,7 @@ def accept(self, graph: CFG, info: SubroutineInfo) -> None: ### 调用约定 -为了简化同学们的实现,实验测例中没有与 gcc 编译的文件相互调用的要求,因此,大家不一定需要实现标准调用约定。你甚至可以完全使用栈来传递参数,而不使用寄存器。但是,如果你想要实现标准调用约定,可以参考下面的内容。 - - +我们给出RISC-V标准调用约定供大家参考,你可以不按照标准调用约定实现,这样的话你需要自己定义一种调用约定。 #### RISC-V 的标准调用约定 @@ -328,7 +265,7 @@ int foo () { ## 实战教学 -我们推荐大家按照以下步骤实现,当然这不是唯一的实现方式。前中端的部分在前面的step中涉及很多,大家应该已经比较熟悉,这里着重关注**后端**要做的事。此处我们采用RISC-V 32的**标准调用约定**,主要使用寄存器进行传参。 +我们推荐大家按照以下步骤实现,当然这不是唯一的实现方式。前中端的部分在前面的step中涉及很多,大家应该已经比较熟悉,这里着重关注**后端**要做的事。 ### 要做什么 @@ -362,7 +299,7 @@ int foo () { - 这是否意味着原本就在caller-saved寄存器中的参数也被丢到了栈上?似乎有些多余? - 是的,但这样处理比较简单。比较理想的方案是直接将参数从一个寄存器复制到目标参数寄存器,但这可能带来一些边角情况。 + 是的,但这样处理比较简单。比较理想的方案是直接将参数从一个寄存器复制到目标参数寄存器,但这可能带来一些边角情况,你需要谨慎处理。 2. **将参数放入寄存器**:所有传参用到的寄存器(`a0`~`a7`)都是caller-saved寄存器,1中的操作保证了传参所需要的寄存器都是空的,因此直接将参数放到寄存器中即可。具体地,用物理寄存器`a0`~`a7`传递被调用函数的前8个参数,我们假设这8个参数对应的临时变量(Temp)为`v0`~`v7`。对于第i个参数,目标是将`vi`的值加载入`ai`。若`vi`已经与某个物理寄存器`xj`绑定,则可以生成指令`mv ai, xj`;如果vi的值不在物理寄存器中,调用`emitLoadFromStack`。(思考: 如果前面暂时不解除volatile寄存器的绑定,这里可能会有什么问题? 你有更高效的解决方案吗?) @@ -380,33 +317,18 @@ int foo () { ### 对于被调用者的处理 -这里我们需要关注源文件`backend/riscv/riscvasmemitter.py`中的`RiscvSubroutineEmitter`类。被调用者需要从正确的位置获取到传入的参数,因此需要处理寄存器和临时变量的对应关系;同时在被调用函数的结尾我们要准确无误地返回到调用处,因此需要处理和返回地址相关的信息。 +这里我们需要关注源文件`backend/riscv/riscvasmemitter.py`中的`RiscvSubroutineEmitter`类和`backend/reg/bruteregalloc.py`中的`BruteRegAlloc`类。被调用者需要从正确的位置获取到传入的参数,因此需要处理寄存器和临时变量的对应关系;同时在被调用函数的结尾我们要准确无误地返回到调用处,因此需要处理和返回地址相关的信息。 1. **处理返回地址**:具体需要保存和恢复`ra`寄存器,相关实现在`emitEnd`函数中。框架的现有部分已经帮助大家处理好了callee-saved寄存器的保存和恢复,你可以参照这部分实现`ra`寄存器的保存和恢复。(备注:严格来讲`ra`并不是callee-saved寄存器。`ra`会在什么情况下被修改?不过你可以选择总是保存和恢复`ra`。) -2. **处理传入的函数参数和临时变量的对应关系**:`RiscvSubroutineEmitter`通过成员`nextLocalOffset`和`offsets`管理临时变量在栈上的位置,现在函数的输入参数对应的临时变量也应当纳入`offsets`的管理。相比于常规临时变量是先定义再使用,函数参数对应的临时变量具有初值、可以直接使用。一般的临时变量在栈上的位置是动态分配的(需要先调用`emitStoreToStack`修改`offsets`),为了方便地保存函数参数对应临时变量的初值,我们推荐一开始就为这些临时变量确定好栈上的位置。而在进入函数主体之前,我们将参数的值取出放到栈上(参数对应的临时变量的栈上所在位置),这样在函数主体的指令看来参数就与其它临时变量没什么区别了。具体而言: - - a. 预先分配函数参数对应的临时变量在栈上的位置。在`RiscvSubroutineEmitter`的初始化函数中能够获取到`SubroutineInfo`,我们认为其中包含了函数参数的数量和它们对应的临时变量等信息。假设前3个函数参数对应的临时变量为`_T0`到`_T2`,通过修改`nextLocalOffset`和`offsets`成员为`_T0,_T1,_T2`提前分配栈空间。 - - b. 将参数取出保存到栈上(设置参数临时变量的初值)。要修改的位置位于`emitEnd`函数中、函数主体开始(输出缓冲区中的指令)之前。对于用寄存器`a0`到`a7`传递的参数,直接将`ai`保存到栈上相应位置即可;而对于栈传递的参数,先将它们加载到一个临时寄存器中,再保存到栈上正确的位置(这里推荐用`Riscv.NativeLoadWord`和`Riscv.NativeStoreWord`)。(备注:对于用寄存器传递的参数,你也可以在寄存器分配过程中直接绑定参数临时变量与相应的物理寄存器,这样也许能够省掉一对load/store。) +2. **处理传入的函数参数和临时变量的对应关系**:将传入的参数与临时变量绑定,这样在函数体中就可以直接使用这些参数。`BruteRegAlloc`类中的的`bindings`变量记录了临时变量和物理寄存器的对应关系,你可以使用`bind`, `unbind`函数来完成这些操作。思考应该在何处进行这个绑定操作。 ### 一些可能带来困惑的地方 1. `ra`是一个caller-saved寄存器,但它有着和callee-saved寄存器相似的处理方式。一般而言只有当某个函数作为caller调用了其它函数时,它存放在`ra`中的返回地址才会被覆盖掉,这与其它caller-saved寄存器类似。然而鉴于`ra`的特殊用途,你可以把它视作一个callee-saved寄存器。 -2. [**bug report**] 避免调用`TACInstr`的`toNative`方法,该方法实现有误,会导致原`dsts`和`srcs`成员被覆盖。助教将于之后修复。 - -3. 你可能会发现我们的框架能支持的栈空间大小有限,存放不了太多的临时变量。目前而言的确是这样,你无需考虑那种情况。 +2. 你可能会发现我们的框架能支持的栈空间大小有限,存放不了太多的临时变量。目前而言的确是这样,你无需考虑那种情况。 - - # 思考题 1. 你更倾向采纳哪一种中间表示中的函数调用指令的设计(一整条函数调用 vs 传参和调用分离)?写一些你认为两种设计方案各自的优劣之处。