代码生成

数组定义

数据的内存空间是连续的,因此无论数组的原型是几维的,都可以看做是一个一维的大数组。例如,对于一个数组 int a[d1][d2][dn]\mathtt{int}~a[d_1][d_2]\cdots[d_n],可看做是 int a[d1d2dn]\mathtt{int}~a'[d_1d_2\cdots d_n]。访问 a[i1][i2][in]a[i_1][i_2]\cdots[i_n],就是访问 a[i1d2d3dn+i2d3d4dn++in]a'[i_1d_2d_3\cdots d_n + i_2d_3d_4\cdots d_n + \cdots + i_n]

我们需要考虑定义数组时,是作为局部变量还是全局变量:

当某个局部变量定义为数组时,数组空间分配在栈上。这与普通变量的定义基本没区别,同样需要算出在栈帧上的偏移量,只不过大小不再是固定的 4 字节,而是数组的元素个数再乘 4 (字节/元素),因此计算其他变量的偏移量时也要做相应修改。由于不支持数组的初始化,分配完栈帧后不用管它就行,即使这段栈空间保留了之前变量的信息,因此数组还有其他未初始化的局部变量的初值都是不确定的。

当某个全局变量定义为数组时,数组空间分配在程序的数据段。同样我们不考虑 BSS 段,直接将其放到 .data 中。作为全部变量时,数组中每个元素默认初始化为 0,可用如下汇编码来将连续一段内存初始化为 0:

    .align 2
a:
    .zero 400

其中 .zero 后面的数字即这段内存的大小(单位字节)。上述汇编码可由定义在全局的 int a[100];int a[10][10]; 生成。

在 RISC-V 指令集中,addilwsw 等 I 型和 S 型指令的立即数大小只有 12 位,因此我们不能定义一个很大的数组作为局部变量,否则那些分配栈帧、Load/Store 局部变量的指令的偏移量就会超出 12 位。你可以实现对这种情况的特殊处理,使用其他指令进行代替,不过我们的测试集保证了不会出现这种情况。

下标运算

指针下标

我们允许对指针和数组类型进行下标运算。首先来看指针的下标运算。指针的下标运算非常类似于解引用运算 *,只是需要加上一个偏移,大小为下标乘上指针基类型的大小,即 p[i] 等价于 *(p + i * size)。另外需要注意,我们在 lab11 中提到了左值的概念,* 运算后的结果可以是左值,类似地,指针的下标运算也可以是左值:

int *p;
p[0] = 1;       // p[0] 是左值
int a = p[1];   // p[1] 是右值
int *q = &p[2]; // p[2] 是左值

于是,我们按照实现解引用运算的方法,即可实现指针的下标运算。

数组下标

然后再来看数组的下标运算。数组的下标运算可以分为两类:

  1. 当下标运算的次数等于数组的维度时,结果是数组中的元素。这种情况与指针的下标运算类似,可以作为右值,结果是数组中元素的值;也可以作为左值,结果是数组中元素的地址。
  2. 当下标运算的次数小于数组的维度时,结果是一个数组。由于任何数组可看做一个一维大数组,此时的结果也可以看做是原数组中的一个子数组。当之后对该结果进行转指针、取下标等运算时,都是相对于这个子数组来说的。之后要取子数组中的哪个元素现在还是未知的,因此无论之后是否有可能成为左值,这一步都应该返回子数组的地址。

对于第 1 种情况,与指针的下标运算类似处理;对于第 2 种情况,关键是要求出子数组的地址。由于下标运算在文法中是递归定义的,我们可以自然地得出地址的计算方法:

  • 对于数组类型的变量 Ident,地址即 Ident 在栈或数据段的地址;
  • 对于子数组 array[i],地址是 array 的地址加上 i * sizeof(array[i])

例如,对于数组 int a[3][4][5],有:

  • a[i] 的地址是 a + (i * 4 * 5) * sizeof(int)
  • a[i][j] 的地址是 a + [(i * 4 * 5) + (j * 5)] * sizeof(int)
  • a[i][j][k] 的地址是 a + [(i * 4 * 5) + (j * 5) + k] * sizeof(int)

总结一下,无论是指针还是数组,无论是左值还是右值,我们都可以用同一套代码框架来生成汇编码:

function visitSubscript(ctx) {          // <postfix> ::= <postfix> '[' expr ']'
    visitPostfix(postfix);              // 计算要取下标的表达式的值,设结果保存在 t0
    visitExpr(expr);                    // 计算下标的值,设结果保存在 t1
    emitMul("t1", "t1", ctx.type.size); // t1 = t1 * sizeof(结果的类型)
    emitAdd("t0", "t0", "t1");          // t0 = t0 + t1,此时的结果即下标运算后子数组或元素的地址
    if (ctx.type == Array || ctx.isLValue) {
        // do nothing                   // 如果结果是数组类型,或是左值,直接返回其地址
    } else {
        emitLoad();                     // 否则,再生成一条 Load 指令来取出该地址中保存的值
    }
}

function visitPrimary(ctx) {
    if (ctx ::= Ident) {        // <primary> ::= Identifier
        emitAddress(Ident);     // 计算变量 Ident 的地址
        if (ctx.type == Array || ctx.isLValue) {
            // do nothing       // 如果变量是数组类型,或是左值,直接返回其地址
        } else {
            emitLoad();         // 否则,再生成一条 Load 指令来取得变量的值
        }
    } else {
        // ...
    }
}

指针算术运算

这一步非常简单,我们要支持的所有指针算术运算就这么多:

  1. 指针加整数,或整数加指针:只需增加一步,将整数操作数乘上指针基类型的大小。
  2. 指针减整数:与指针加法一样,只需增加一步,将整数操作数乘上指针基类型的大小。
  3. 指针减指针:先直接做减法,然后除以指针基类型的大小(要求两指针基类型相同)。

由于指针的基类型只能是 int 或指针,其大小都是 4 字节,你可以用 sllisrai 指令进行左移、右移来代替乘除运算。

results matching ""

    No results matching ""

    results matching ""

      No results matching ""