step2 实验指导

我们按照上一节划分的编译器阶段,分阶段给出 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

思考题

  1. 我们在语义规范中规定整数运算越界是未定义行为,运算越界可以简单理解成理论上的运算结果没有办法保存在32位整数的空间中,必须截断高于32位的内容。请设计一个 minidecaf 表达式,只使用-~!这三个单目运算符和从 0 到 2147483647 范围内的非负整数,使得运算过程中发生越界。

提示:发生越界的一步计算是-

总结

本步骤中其他运算符的实现逻辑和方法与取负类似,大家可以借鉴取负的实现方法实现剩下的逻辑非和按位非。并且,我们在实验框架中已经给出了取负的参考实现,希望能够帮助大家快速上手编译实验。

results matching ""

    No results matching ""

    results matching ""

      No results matching ""