Skip to content

北京大学编译原理课程Lab (Course Lab of PKU Compiler Course) (2024 Spring),由 Rust 语言实现 (C++ is dead, long live Rust)。这是一个编译器,从 SysY 语言(C语言的子集)到 Koopa IR 中间表示,再到 RISC-V 汇编代码。文档记录了我的 lab 完成过程,也许能提供一些帮助。

Notifications You must be signed in to change notification settings

xhzgenius/Compiler-Lab

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

86 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Compiler-Lab

北京大学编译原理课程lab(个人代码)(Rust实现)

Usage

挂载目录到容器:
docker run -it -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev

然后可以用 VS Code 左下角的“打开远程窗口”,选择“Attach to a running container”,即可在 VS Code 图形界面访问容器,并方便地进行测试。

容器内的测试方法:

./autotest [-koopa|-riscv|-perf] [-t TEST_CASE_DIR] [-w WORKING_DIR] REPO_DIR

参数:

  • -koopa|-riscv|-perf: 可选, 指定对编译器进行何种测试, 可为 Koopa IR 测试 (-koopa), RISC-V 测试 (-riscv) 和性能测试 (-perf). 默认为 -koopa.
  • -t TEST_CASE_DIR: 可选, 指定测试用例目录, 目录内须包含若干 .c/.sy 文件 (也可位于子目录中). 其中, 每个 .c/.sy 文件应当有一个位于相同目录中且同名的 .out 文件, 对应该 SysY 程序的预期输出/返回值; 每个 .c/.sy 文件还可以有一个位于相同目录中且同名的 .in 文件, 对应该 SysY 程序的输入, 如果程序不会从 stdin 读取输入, 则该文件应该省略. (参考开放测试用例的写法). 默认为脚本目录下的 testcases 目录.
  • -w WORKING_DIR: 可选, 指定工作目录. 脚本会把编译生成的文件以及其他各类文件存入工作目录, 而默认情况下, 工作目录是一个临时目录, 脚本运行结束后会将其删除. 如果你不想每次都让脚本重新编译你的项目, 而是需要增量编译, 你可以指定一个工作目录, 此时脚本退出前不会删除这个目录.
  • -s WORKING_DIR: 可选, 指定运行测试用例目录下的子目录中的测试用例.
  • REPO_DIR: 必选, 指定编译器仓库的目录. 目录内须包含 Makefile/CMakeLists.txt/Cargo.toml 文件之一, 脚本将自动使用 make/CMake/Cargo 构建编译器, 并对其进行测试.

本人使用自定义的测试样例(位于 Compiler-Lab-Test-Suites 目录中,且只希望运行其中 functional_test 子目录的测试样例),测试命令如下:

autotest -riscv -t Compiler-Lab-Test-Suites -s functional_test /root/compiler >log.txt 2>&1 &

>log.txt 2>&1 & 代表将标准输出与标准错误结果重定向到 log.txt ,且后台运行。但是如果某个样例测试超时,后面的样例还是会输出到命令行,这可能是bug。

在本地运行编译器:
cargo run -- -koopa hello.c -o hello.koopa

cargo run -- -riscv hello.c -o hello.asm

Lab记录

Lv.1 main函数

配置依赖环境

Cargo.toml 中添加对应依赖,并在项目根目录新建 build.rs,参照 在线文档此处 。注意将lalrpop的版本改成 crates.io 上面的最新版本,我写lab时最新版本为0.20.2。

运行示例代码

将在线文档里的lalrpop代码写入 src 目录中新建的 sysy.lalrpop 中,rust代码写入 main.rs 中,并把要编译的源代码写入 hello.c

然后使用

cargo run -- -koopa hello.c -o hello.koopa

编译运行。

写自己的代码

新建 ast_def.rs ,在其中定义AST(抽象语法树)的结构体。

更改 sysy.lalrpop ,在其中将返回值改为所定义的结构体。

创建一个子模块 ir_builder ,负责生成IR,在 mod.rs 里定义解析IR的函数入口,新建一个 build.rs ,在里面为 ast_def.rs 里定义的结构体实现生成IR的方法。

测试命令:
docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv1 /root/compiler

同时, 在运行测试前, 你需要确保你的编译器 (假设名称为 compiler) 能处理如下的命令行参数:

compiler -koopa 输入文件 -o 输出文件

其中, -koopa 代表你的编译器要输出 Koopa IR 文件, 输入文件 代表输入的 SysY 源文件的路径, 输出文件 代表 Koopa IR 的输出文件路径. 你的编译器应该解析 输入文件, 并把生成的 Koopa IR 输出到 输出文件 中.

测试程序会使用你的编译器将输入编译为 Koopa IR, 然后借助 LLVM 将 Koopa IR 进一步编译成可执行文件. 最后, 测试程序执行可执行文件, 检查程序的返回值 (也就是 main 的返回值) 是否符合预期. 测试程序不会检查你输出的 Koopa IR 的形式, 你输出的 IR 只要功能正确, 即可通过测试.

关于实验环境/测试脚本的详细使用方法, 请参考实验环境使用说明. 关于调试编译器的相关思路, 请参考调试你的编译器. 关于测试脚本的工作原理, 请 RTFSC.


Lv.2 初试目标代码生成

说明

上一节我生成的IR直接就是内存中的,无需从字符串IR转化成内存IR。

如何遍历内存中的IR,参考:这里

“Layout” 直译的话是 “布局”. 这个词不太好用中文解释, 虽然 Koopa IR 的相关代码确实是我写的, 我也確實是個平時講中文的中國大陸北方網友.

比如对于基本块的指令列表: 指令的数据并没有直接按照指令出现的顺序存储在列表中. 指令的数据被统一存放在函数内的一个叫做 DataFlowGraph 的结构中, 同时每个指令具有一个指令 ID (或者也可以叫 handle), 你可以通过 ID 在这个结构中获取对应的指令. 指令的列表中存放的其实是指令的 ID.

这么做看起来多套了一层, 但实际上 “指令 ID” 和 “指令数据” 的对应关系, 就像 C/C++ 中 “指针” 和 “指针所指向的内存” 的对应关系, 理解起来并不复杂. 至于为什么不直接把数据放在列表里? 为什么不用指针或者引用来代替 “指令 ID”? 如果对 Rust 有一定的了解, 你应该会知道这么做的后果…

写自己的代码

新建子模块 assembly_builder ,负责将IR转换成汇编语言。在其中为每种Koopa IR结构实现转换成汇编语言的功能。

修改 main.rs ,使得命令行参数能识别 -koopa-riscv 参数。

测试
docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv1 /root/compiler

你需要将 项目目录 替换为你的编译器项目在宿主机上的路径. 同时, 在运行测试前, 你需要确保你的编译器 (假设名称为 compiler) 能处理如下的命令行参数:

compiler -riscv 输入文件 -o 输出文件

其中, -riscv 代表你的编译器要输出 RISC-V 汇编文件, 输入文件 代表输入的 SysY 源文件的路径, 输出文件 代表 RISC-V 汇编的输出文件路径. 你的编译器应该解析 输入文件, 并把生成的 RISC-V 汇编输出到 输出文件 中.

为了同时兼容 Koopa IR 和 RISC-V 的测试, 你的编译器应该能够根据命令行参数的值, 判断当前正在执行何种测试, 然后决定只需要进行 IR 生成, 还是同时需要进行目标代码生成, 并向输出文件中输出 Koopa IR 或 RISC-V 汇编.


Lv.3 表达式

写自己的代码
前端:IR生成

ast_def.rssysy.lalrpop 中添加新的文法定义。改用了enum类型,配上rust的match表达式真的太tm好用了。只要能成功编译,几乎一遍过,简直是bug-free编程,太吊了。

build_ir.rs 中将定义的所有struct递归地转化成 Koopa IR 。

递归地计算表达式的时候,将两棵子树算出来的结果(Koopa IR 中的 value)用Koopa IR中的BinaryOP计算出新的结果。

我的递归函数没有返回值,所以用一个全局变量来记录子树的value,每次计算出子树的value就存到全局变量,然后返回到上层函数再取出来使用。

后端:目标代码生成

在Lv. 3中,所有指令的计算结果都放在一个寄存器中。关于寄存器是否会被用完的问题,只要及时释放掉不用的寄存器即可,反正 Lv. 3 没有局部变量,表达式的中间值用完了都可以释放掉。

寄存器 ABI 名称 描述 保存者 寄存器 ABI 名称 描述 保存者
x0 zero 恒为 0 N/A x8 s0/fp 保存寄存器/帧指针 被调用者
x1 ra 返回地址 调用者 x9 s1 保存寄存器 被调用者
x2 sp 栈指针 被调用者 x10-11 a0-1 函数参数/返回值 调用者
x3 gp 全局指针 N/A x12-17 a2-7 函数参数 调用者
x4 tp 线程指针 N/A x18-27 s2-11 保存寄存器 被调用者
x5 t0 临时/备用链接寄存器 调用者 x28-31 t3-6 临时寄存器 调用者
x6-7 t1-2 临时寄存器 调用者

没什么花头,老老实实分类然后写汇编代码就行。

Binary 运算的左右两边的 value ,可以根据 lhs() 和 rhs() 获得。只要维护一下给每个 value 分配了哪个寄存器就行。

注意

Koopa IR 没有逻辑 and 和 or 运算符,因此使用 !=0 先把整数转换成布尔值,再进行位运算的 && 或者 || 操作。

RISCV 没有 le 和 ge ,因此转换成 not gt 和 not lt ;同时也没有 eq 和 neq ,因此先和自己异或,再用 seqz 或 snez 比较是否为0。

最后一个本地样例的表达式比较多,因此要把不用的寄存器释放掉。维护一个表即可。

测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv3 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv3 /root/compiler

我从 https://github.com/segviol/indigo/ 获得了大量的测试样例以及一个测试脚本。

用法写在 test.py 开头注释。


Lv. 4 常量和变量

写自己的代码
前端:IR生成(这一步暂时将所有常量当作变量处理,不计算常量表达式)

ast_def.rssysy.lalrpop 中添加新的文法定义。

如何匹配0个或多个的示例:

Block: Block = "{" <block_items: (BlockItem)*> "}" => Block::Default(block_items); // Referenced from kira-rs. 

ConstDecl: ConstDecl = "const" <b: BType> <c: ConstDef> <cs: ("," <ConstDef>)*> ";" => ...

由于文法太多,把所有文法拆成了三个部分,放在一个module ast_def 里面。

把前端生成IR的代码也拆成了三个部分。

维护一个HashMap作为符号表。符号表里记录了该符号的类型和对应的Koopa IR Value

前端:计算常量表达式(包括非常量表达式中的常量部分)

我修改了 build() trait 的返回值,现在它返回两种情况:一个 i32 常量,或者一个变量的 Koopa IR Value

这样就可以递归地计算出那些编译期已知的常量部分。如果一个二元求值表达式的两个操作数都是 i32 常量,就直接计算结果,而不是把它们写进中间表示代码。

后端:目标代码生成

在线文档让我们把所有局部变量和临时变量都存到栈里。我觉得这样太sb了,只要存局部变量进行了,临时变量用完了就可以扔了。

在进入函数时,先扫描一遍全部指令,数一下要分配多少空间给局部变量,然后移动栈指针( sp 寄存器)即可,同时用一个 HashMap 记录一下每个局部变量符号对应的栈帧里的位置。

然后在遍历指令生成汇编代码的时候,对于Koopa IR中的每句 loadstore 指令,生成RISC-V中对应的 lwsw 指令就行了。( loadstore 指令所操作的变量名可以用 ValueData 中的 name() 方法获得。)

注意
前端:IR生成

dfg.new_value().load() 函数返回的是一个含有值的 Value ,而 dfg.new_value().alloc()dfg.new_value().store() 函数返回的是一个指针的 Value

关于左值,当左值用来计算(成为PrimaryExp)的时候再 load() ,其它时候不要急着 load() ,将它当作指针。

Koopa IR 是强类型的,所以当指针和值进行运算的时候会 panic

还有个坑:有个在线样例估计有多个 return 语句,需要忽略第一个 return 语句之后的所有内容,否则评测不通过。

本地测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv4 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv4 /root/compiler

Lv. 5 语句块和作用域

写自己的代码

非常简单,只需要把符号表搞成一个栈就行,在对 Block 进行 build() 的时候往符号表栈里加入一个新的表, build() 结束后删掉新的表。查找符号的时候从后往前查找即可。

这里我重构了代码,把包含 build() 方法的 trait 从原来的 IRBuildable 一类改成了 IRBuildableIRExpBuildable 两类。前者返回 build 的结果状态(是成功或者是提前终止,为了处理 Block 中有返回语句的情况),后者返回一个枚举类,其可能为 build 出来的 Value 或者提前计算出的常量值。

同时,return 语句可能没有返回值了,这里也要判断处理一下。

注意

Koopa IR 函数内部不能定义相同名字的变量,所以为了避免名称碰撞,要手动给变量重命名。我给变量名加了个后缀——嵌套的深度,也就是说 @a 会变为 @a-1@a-2 ,... 。这里我规定最外层的全局变量深度为 0

还要注意 Block 中可能会提前 return 的问题,这个也要处理。

根据观察发现,当使用 set_value_name 尝试让 Koopa IR 中出现多个名称相同的符号时,Koopa IR 会自动给新的名称加个后缀 xx_0xx_1 等,所以无需考虑一个语句块内定义多个 同名变量的问题。

本地测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv5 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv5 /root/compiler

Lv.6 if 语句

写自己的代码
首先,加上 if 语法。

这玩意有二义性,把它改成无二义性文法的改法很恶心,但也只能这么做。改法如下:

//! Stmt ::= UnmatchedStmt
//!        | MatchedStmt
//! 
//! UnmatchedStmt ::= "if" "(" Exp ")" MatchedStmt ["else" UnmatchedStmt]
//! 
//! MatchedStmt ::= LVal "=" Exp ";"
//!        | [Exp] ";"
//!        | Block
//!        | "if" "(" Exp ")" MatchedStmt "else" MatchedStmt
//!        | "return" [Exp] ";";

然而我并不想把实际上相同的 build() 方法写两遍(事实上也应尽量不把重复的代码写两遍)。UnmatchedStmtMatchedStmt 只有在解析语法的时候有区别,在此之后没任何区别,因此我定义 UnmatchedStmtMatchedStmt 内部都是 BasicStmt ,在解析完语法之后,一视同仁地处理它们。

其次,如何在 Koopa IR 中实现 if else 跳转?

使用 Koopa IR 中的基本块 BasicBlock 即可。Koopa IR 有 branch 和 jump 的指令,跳转的目的地都是 BasicBlock 。在 if 语句之前有一个初始块,if 的条件为 true 时有一个基本块,if 的条件为 false 时有一个基本块,if 语句结束时也有一个基本块。因此,除去初始的基本块以外,我还要创建三个新的基本块。通过跳转即可实现 if else 的功能。

build 各部分的 Koopa IR 的时候,我会指定当前所处的基本块是哪个,然后调用 build 方法。这样就能把各部分分别 build 到正确的基本块中。

最后,如何实现短路求值?

好问题。

就这玩意写了我整整四个小时,太恶心了。最后终于找到一种比较优雅的办法。

对于 exp1 || exp2 这样的表达式,我分3类处理:

exp1 为非常量表达式时,则转化为

int result = 0;
if (lhs != 0) {
	result = rhs!=0;
}

来处理短路求值;

exp1 为常量表达式时,若其计算出的值为0,则直接使用 exp2 的结果作为整个表达式的结果;若计算出的值不为0,则整个表达式的值为常量1。

注意
关于命名

测试脚本不允许 Koopa IR 的变量名或者基本块名带有横杠,明明 Koopa IR 规范是允许的。

于是我又把变量命名方法改掉了。直接用原始变量名,因为 Koopa IR 不仅不限制重复名称,并且生成 IR 的时候会自动帮你避免重复名称。

关于测试样例

有些测试样例规模很大,于是我就爆寄存器了。。。而且在线平台不给错误详细信息,所以一直查不出原因。。。

妈的,不得不写寄存器分配算法了。。。

妈的,就这么个玩意花了我四五个小时。。。

所有的 Value 都被我保存在栈上。当要访问一个 Value 时,有几种可能:

第一,Value 是一个常量,分配一个新的寄存器,用 li 指令将常量加载到这个寄存器,然后返回这个寄存器;

第二,Value 是一个变量,已经在寄存器中,直接返回这个寄存器。

第三,Value 是一个变量,且不在寄存器中,分配一个新的寄存器,用 lw 指令将 Value 加载进来,返回这个寄存器。

分配新的寄存器的时候,如果要踢掉已有的其它 Value ,那就还要用 sw 指令将其存回栈上。

踢掉的寄存器选择方法为LRU(Least Recently Used)。

本地测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv6 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv6 /root/compiler

Lv.7 while 语句

写自己的代码
新增语法:
//! UnmatchedStmt ::= ...
//!                 | "while" "(" Exp ")" UnmatchedStmt;
//!
//! MatchedStmt ::= ...
//!               | "while" "(" Exp ")" MatchedStmt
//!               | "break" ";"
//!               | "continue" ";"
//!               | ...

为了避免 if else 语句带来的二义性,这里也要把 MatchedStmtUnmatchedStmt 分开写。

新增的 while 语句只需要在 BasicStmt 中一起处理。

具体实现

和 if 很像,也是创建三个 BasicBlock 。需要手动维护两个栈,记录当前如果 break 或者 continue 的话会跳转到哪个基本块。

注意

如果在 while 的内部已经确定了会 break 或者 continue ,那么就不要在基本块末尾加上额外的跳转语句,要不然 Koopa IR 会报错。

本地测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv7 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv7 /root/compiler

Lv.8 函数和全局变量

卧槽,大的要来了!

写自己的代码:Koopa IR 生成
搞定语法冲突

Lalrpop搞不定文档里的语法,因为会产生 BTypeFuncType 的规约-规约冲突。我直接简单粗暴把这俩合二为一了。

然后费了一番功夫把语法解析搞定了。新增的语法也太多了。。。

全局变量

VarDecl build() 的时候,判断一下当前是否处于函数内,分类处理即可。

然后我惊喜地发现,ConstDecl build() 完全不用改就能处理全局常量。

函数定义

首先要维护一个全局函数表。

传参的时候,实参传进来的 Value 是具体的值,不是指针(左值)。

在进入函数体的时候,首先要给每个形参 alloc 一下把它们变成左值,然后用 store 把实参赋值给形参,然后给新的符号表里面加上所有的形参。

函数调用

修改 UnaryExp 加上对应内容即可。调用函数的时候比较简单。

库函数

直接加入

decl @getint(): i32
decl @getch(): i32
decl @getarray(*i32): i32
decl @putint(i32)
decl @putch(i32)
decl @putarray(i32, *i32)
decl @starttime()
decl @stoptime()

是不是就行了(?)

还真是。更优雅的办法是在 CompUnit build() 的时候使用 koopa::ir::FunctionData::new_decl() 在开头添加函数声明。

写自己的代码:目标代码(RISC-V)生成
全局变量

首先,每个 build 方法都要额外增加一个 program 参数,因为要通过 program.borrow_value(value) 来找到全局变量对应的 Value

其次,在 koopa::ir::ValueKind::Load 的处理中,要检查这个 load 的源是全局变量还是局部变量。检查方法是 load.src().is_global()

函数调用

首先计算存放函数调用参数需要的栈空间的最大值,从而算出新的栈帧长度。

并且,存放局部/临时变量的位置也要注意,应当放在函数调用参数的上方。

目前的栈帧分为三部分:最上面是保存的 ra 寄存器,然后是局部/临时变量,最后是函数调用参数。

然后修改 FunctionDatabuild() 方法,首先把所有的实参加入栈帧查找表及寄存器查找表,然后在 prologue 中保存 ra 寄存器。这样,就能编译函数体了。

最后修改 koopa::ir::ValueKind::Call 这部分即可:先保存 caller-saved 寄存器,然后把函数调用参数放到对应的位置,最后使用 call 指令调用函数。

命名规范

经过 kira-rs 示例编译器测试,源代码中的全局变量与函数不允许重名。

我的编译器允许全局常量与全局变量重名。当前代码中谁最后声明就使用谁。

经过 kira-rs 示例编译器测试,RISC-V 的 label 可以以 "." 开头。

因此,对于所有带名字的基本块,我以 "." 作为 label 开头,全局符号均用原始名称作为 label 。

注意

有的时候源代码里没有 return 语句,要手动补上。

使用自己的测试样例,定位到已知错误:寄存器分配有问题。

比如:我在 if 的一个分支没有给 value @a 分配寄存器,另一个分支为 @a 分配了寄存器 t5,if 结尾的时候就会取出 t5 的值作为 @a 的值,这是错误的。

重新思考寄存器分配,以及写策略

Koopa IR 中,变量分为两类:一类是定义的变量(包括全局变量),以 @ 开头,每次使用必定伴随 load 和 store ;另一类是临时变量,以 % 开头,会被唯一赋值/使用一次。如果把两类统一用 LRU Cache 来保存,就会出现上述的不同条件分支中 Cache 位置不同的错误。

解决办法:在出基本块时(也即遇到 Return Jump Branch 指令时),保存所有的定义变量;遇到 Call 指令时,保存所有的全局变量。

对于临时变量,用完就从表里删除。

本地测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv8 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv8 /root/compiler

这些测试是远远不够的,因此还是得使用开头提到过的自己的测试样例。

Lv.9 数组

我去,更大的来了

写自己的代码

首先更新文法定义。一共三处:数组变量声明、数组初始化表达式,以及数组左值。

然后更新语义分析。

新的左值

左值有两种用途:一是作为 PrimaryExp 参与运算,二是在赋值语句中作为目标地址。

原先左值的 build 结果只有两种:变量(是一个指针,需要通过 load 来访问)常量(直接编译期求值)。

现在,左值的 build 结果变为三种:地址(是一个指针,需要通过 load 来访问),可直接访问的变量(无需通过 load 来访问),常量(直接编译期求值)。

%2 = getelemptr @a, 0 这种地址的计算结果就是可直接访问的变量。

PrimaryExpbuild 阶段,对这三种左值分别处理(使用 load 指令将其加载/不做处理/创建一个新的整数常量 Value )。

在赋值语句的 build 阶段,检查左值是否为地址,如果不是的话返回错误。

初始化表达式

妈的,这玩意真的难搞

我写了个递归函数处理这玩意。详见代码吧。

增加更多检查与错误信息(语义分析阶段)

函数调用的参数数量检查/参数类型检查,若不通过则返回错误。

赋值语句的左值检查,若不是地址,返回错误。

全局变量的定义阶段,若初始值出现非常量表达式,返回错误。

全局变量/函数的定义阶段,若出现重复符号,返回错误。

常量的定义阶段,初始值出现非常量表达式,返回错误。

函数调用的函数名不在符号表中,或者左值的变量名不在符号表中,返回错误。

数组的声明时,若各维度长度中出现非常量表达式,返回错误。

数组的初始化时,若aggregate对齐错误/出现非常量表达式,返回错误。

break 或者 continue 语句出现在了错误的位置,返回错误。

拆分局部数组的初始化

为了后面部分考虑,把局部数组的初始化从原来的一条 store 指令拆分成了多条 store 指令,每条放一个数。

目标代码(RISC-V汇编代码)生成

目前进度:全局数组初始化没写。

有些 Value 莫名其妙找不到。哦,原来是本来就从表中移除的临时变量啊,那没事了。

本地测试

测试 Koopa IR:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -koopa -s lv9 /root/compiler

测试 RISC-V 汇编:

docker run -it --rm -v D:/MyCodes/Compiler-Lab:/root/compiler maxxing/compiler-dev autotest -riscv -s lv9 /root/compiler

—END—

About

北京大学编译原理课程Lab (Course Lab of PKU Compiler Course) (2024 Spring),由 Rust 语言实现 (C++ is dead, long live Rust)。这是一个编译器,从 SysY 语言(C语言的子集)到 Koopa IR 中间表示,再到 RISC-V 汇编代码。文档记录了我的 lab 完成过程,也许能提供一些帮助。

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages