我们按照上一节划分的编译器阶段,分阶段给出 step2 实验指导。本实验指导使用的例子为:
需要注意的是,我们为了简化描述,提取出了测试用例中和本步骤最相关的部分,实际的测试用例还是一个完整的,带有主函数的 MiniDecaf 程序。
-1
在 step2 中,我们引入了一元运算,因此需要引入新的抽象语法树节点:
节点 | 成员 | 含义 |
---|---|---|
Unary |
操作数 operand ,运算类型 op |
一元运算 |
注意由于各种一元运算的形式是一样的,只是运算规则不同,所以用统一的一元运算节点来表示,在后续步骤中,再根据具体的运算种类翻译为不同的 TAC 与 RISC-V 指令。
由于现在 return 语句的返回值不再是整型常量,而是表达式,因此语义分析时需要递归地访问运算操作结点的子结点,才能访问到作为叶子结点的整型常量,完成 step1 中实现的整型常量越界检查。
代码没有特别需要修改的地方。
在 step1 中,我们只需为 return 语句的返回的整型常量分配一个临时变量即可。而从 Step2 开始,语法树上出现了各种运算操作结点。在生成 TAC 的过程中,我们需要为运算结点分配一个临时变量,并生成一条指令,该指令根据子结点的临时变量进行计算,将结果赋予该结点的临时变量。
针对取负操作,我们显然需要设计一条中间代码指令来表示它,给出的参考定义如下:
请注意,TAC 指令的名称只要在你的实现中是一致的即可,并不一定要和文档一致。
指令 | 参数 | 含义 |
---|---|---|
NEG |
T0 |
对参数取负 |
按照上文说的,-1 在语法树上对应父-子两个结点,父结点为取负操作,子结点为常量 1。在生成过程中,首先使用 Visitor 模式递归地访问子结点,我们使用一个临时变量加载该立即数。之后,在父结点,我们根据子结点的临时变量,生成一条取负指令,将这条指令得到的目标临时变量设置为父结点的临时变量。
因此,测例可以翻译成如下的中间代码:
_T0 = 1
_T1 = NEG _T0
step2 目标代码生成步骤的关键点在于,针对中间代码指令,选择合适的 RISC-V 指令来完成翻译工作。以 NEG 中间表达指令为例,RISC-V 汇编中有 neg 指令与其对应,因此上述中间代码可以翻译为如下的 RISC-V 汇编:
li t0, 1
neg t1, t0
关于目标代码生成有一个小技巧,如果你实在不知道某个运算符应该翻译成怎样的汇编代码,可以参考 gcc 的输出结果。例如,你可以通过 gcc 编译如下程序来了解如何翻译逻辑非运算符到 RISC-V 汇编 riscv64-unknown-elf-gcc -march=rv32im -mabi=ilp32 foo.c -S -O3 -o foo.s
(记得加 -O3 选项):
int foo(int x) {
return !x;
}
不出意外你会获得如下结果:
foo:
seqz a0,a0
ret
- 我们在语义规范中规定整数运算越界是未定义行为,运算越界可以简单理解成理论上的运算结果没有办法保存在32位整数的空间中,必须截断高于32位的内容。请设计一个 minidecaf 表达式,只使用
-~!
这三个单目运算符和从 0 到 2147483647 范围内的非负整数,使得运算过程中发生越界。
提示:发生越界的一步计算是
-
。
本步骤中其他运算符的实现逻辑和方法与取负类似,大家可以借鉴取负的实现方法实现剩下的逻辑非和按位非。并且,我们在实验框架中已经给出了取负的参考实现,希望能够帮助大家快速上手编译实验。