编译器实现之旅——第十四章 变量存取的代码生成器分派函数的实现

在上一章的旅程中,我们已经实现了if语句和while语句的代码生成器分派函数,而在这一章的旅程中,我们将要实现变量存取相关的代码生成器分派函数。要讨论变量存取,我们就需要对变量存取时的栈内存情况具有充分的了解。所以接下来,就让我们先来认识一下:在变量存取过程中所涉及到的栈内存情况吧。

1. 与变量存取相关的栈内存

首先,我们需要明确的是:任何变量的存取行为,一定发生于一个正在被调用的函数中。也就是说,在实现变量存取的过程中,我们必须同时考虑对局部变量和对全局变量的存取,且局部变量将隐藏全局变量。那么,局部变量和全局变量在栈的哪儿?我们又该如何找到这些位置呢?下图便向我们展示了这两个问题的答案:

假设代码中存在全局变量:

    int a;
    int b[3];
    int c;

且函数F存在形参:

    int a, int b, int c

以及局部变量:

    int d;
    int e[3];
    int f;

则我们可以得到符号表:

__GLOBAL__: (a: 0), (b: 1), (c: 5)
F:          (a: 0), (b: 1), (c: 2), (d: 3), (e: 4), (f: 8)

在某次函数F的调用过程中,栈内存结构如下:

+-------+-----+-----+-----+-----+-----+-----+  ...  +-------+-------+-------+-------+-------+-------+-------+-------+-------+---------+-------+-----+  ...
| 索引值 |  0  |  1  |  2  |  3  |  4  |  5  |  ...  | N - 8 | N - 7 | N - 6 | N - 5 | N - 4 | N - 3 | N - 2 | N - 1 |   N   |  N + 1  | N + 2 | ... |  ...
+-------+-----+-----+-----+-----+-----+-----+  ...  +-------+-------+-------+-------+-------+-------+-------+-------+-------+---------+-------+-----+  ...
|   值  |  ?  |  2  |  ?  |  ?  |  ?  |  ?  |  ...  |   ?   |   ?   |   ?   |   ?   | N - 7 |   ?   |   ?   |   ?   |   ?   | BP(Old) |  IP   | ... |  ...
+-------+-----+-----+-----+-----+-----+-----+  ...  +-------+-------+-------+-------+-------+-------+-------+-------+-------+---------+-------+-----+  ...
           ^     ^     ^     ^     ^     ^              ^       ^       ^       ^       ^       ^       ^       ^       ^
           |     |     |     |     |     |              |       |       |       |       |       |       |       |       |
           a     b    b[0]  b[1]  b[2]   c              f      e[0]    e[1]    e[2]     e       d       c       b      a/BP

这幅图对我们而言非常重要,其是这一章与接下来两章的旅程中都会用到的示意图。有了这幅图,我们就可以解释很多在前面走过的旅程中无法解释的事情了。请看:

  1. 在生成符号表的过程中,我们对每个变量进行的看似随意的编号,实际上正对应着这个变量在栈中的位置。对于全局变量而言,符号表中的变量编号就是这个变量在栈中的索引值;而对于局部变量而言,我们可以用"BP - 符号表中的变量编号"得到这个变量在栈中的索引值。这也解释了为什么虚拟机的LD指令需要使用"BP - AX"这样的实现
  2. 对于数组变量而言,数组变量自身的值是这个数组的第一个元素在栈中的索引值。由此我们可以解释:在生成数组变量的符号表编号时,之所以需要将编号额外的再加上数组长度,正是为了让数组后面的变量的索引值不仅越过数组变量自身,还越过数组的内容
  3. 我们只需要进行一次访问即可得到任何非数组变量的值;而对于数组变量,我们就必须先得到这个数组的第一个元素在栈中的索引值,再将此索引值进行偏移,最终才能得到数组中某个元素的值
  4. 显然,这幅图并不是凭空想象画出来的模型图,而是真实存在的,其需要通过代码生成器进行实现。这部分内容我们将在下一章的旅程中进行讨论。这里,我们直接使用这幅图带给我们的结论即可

接下来,就让我们利用上述示意图与结论,来实现变量存取相关的代码生成器分派函数吧。

2. Var节点的分派函数的实现

Var节点用于进行一次变量访问。通过上一节的讨论我们知道,对于一个变量而言,我们需要同时关注两个问题,这两个问题都会影响到变量访问的实现:

  1. 这个变量是一个局部变量,还是一个全局变量?
  2. 这个变量是不是一个数组变量?

对于第一个问题,我们的答案显而易见:如果在当前函数的符号表中能够查到这个变量名,就说明这个变量是一个局部变量;否则,这个变量就是一个全局变量。而对于第二个问题,我们的答案也不难想到:如果Var节点含有两个子节点,则这个变量是一个数组变量;否则,这个变量就不是一个数组变量。

有了上述思考作为铺垫,我们就可以开始讨论Var节点的分派函数的实现了。请看:

vector<pair<__Instruction, string>> __CodeGenerator::__generateVarCode(__AST *root, const string &curFuncName) const
{
    /*
        __TokenType::__Var
            |
            |---- __TokenType::__Id
            |
            |---- [__Expr]
    */

    vector<pair<__Instruction, string>> codeList;

    // Local variable
    if (__symbolTable.at(curFuncName).count(root->__subList[0]->__tokenStr))
    {
        codeList.emplace_back(__Instruction::__LDC, to_string(__symbolTable.at(curFuncName).at(root->__subList[0]->__tokenStr).first));
        codeList.emplace_back(__Instruction::__LD, "");
    }
    // Global variable
    else
    {
        codeList.emplace_back(__Instruction::__LDC, to_string(__symbolTable.at("__GLOBAL__").at(root->__subList[0]->__tokenStr).first));
        codeList.emplace_back(__Instruction::__ALD, "");
    }

    // Array
    if (root->__subList.size() == 2)
    {
        auto exprCodeList = __generateExprCode(root->__subList[1], curFuncName);

        codeList.emplace_back(__Instruction::__PUSH, "");
        codeList.insert(codeList.end(), exprCodeList.begin(), exprCodeList.end());
        codeList.emplace_back(__Instruction::__ADD, "");
        codeList.emplace_back(__Instruction::__POP, "");
        codeList.emplace_back(__Instruction::__ALD, "");
    }

    return codeList;
}

首先,正如上文中所说,我们通过在当前函数的符号表中查找这个变量名,来确定这个变量是一个局部变量,还是一个全局变量,同时,我们也可以得到这个变量在符号表中的编号。所以,我们首先通过一条LDC指令,将变量的编号装载入AX中;然后,根据变量是一个局部变量还是一个全局变量,我们分别使用一条LD或ALD指令,将变量的值装载入AX中。此时,AX中的值可能有两种情况:

  1. 如果这个变量不是一个数组变量,则AX中的值就是变量的值,代码已经生成完毕
  2. 反之,如果这个变量是一个数组变量,则AX中的值只是这个数组的第一个元素在栈中的索引值。我们必须继续进行一次索引值偏移操作,才能得到真正的索引值

所以接下来,如果我们发现当前的Var节点含有两个子节点,我们就需要继续对AX进行索引值偏移操作。首先,我们为Var节点的第二子节点生成代码,显然,只要这段代码运行完毕,一个索引值偏移量就会被装载入AX中,然后,我们只需要再进行一次加法,就能得到真正的索引值了。但请不要着急,我们首先需要将当前的AX压栈,保护起来,以作为加法的第一操作数;然后,我们才能装载计算第二操作数的代码;此时,第一操作数位于栈顶,而第二操作数位于AX,我们就可以执行ADD指令了;这条指令执行完毕后,AX中就装载了偏移后的索引值;此时,不要忘了再执行一次POP指令,以弹出先前我们压入的加法的第一操作数;最后,我们执行一次ALD指令,将数组变量的值装载入AX中。

3. 赋值操作的分派函数的实现

赋值操作的实现思路和变量访问的实现思路是几乎一致的,只是用于变量访问的LD和ALD指令需要换为ST和AST指令。请看:

vector<pair<__Instruction, string>> __CodeGenerator::__generateAssignCode(__AST *root, const string &curFuncName) const
{
    /*
        __TokenType::__Expr
            |
            |---- __Var  -> Root
            |
            |---- __Expr -> AX
    */

    vector<pair<__Instruction, string>> codeList {{__Instruction::__PUSH, ""}};

    // Local variable
    if (__symbolTable.at(curFuncName).count(root->__subList[0]->__tokenStr))
    {
        codeList.emplace_back(__Instruction::__LDC, to_string(__symbolTable.at(curFuncName).at(root->__subList[0]->__tokenStr).first));

        // Scalar
        if (root->__subList.size() == 1)
        {
            codeList.emplace_back(__Instruction::__ST, "");
        }
        // Array
        else
        {
            auto exprCodeList = __generateExprCode(root->__subList[1], curFuncName);

            // Get the (start) pointer (is already an absolute address)
            codeList.emplace_back(__Instruction::__LD, "");
            codeList.emplace_back(__Instruction::__PUSH, "");

            codeList.insert(codeList.end(), exprCodeList.begin(), exprCodeList.end());

            // Pointer[Index] (Pointer + Index)
            codeList.emplace_back(__Instruction::__ADD, "");
            codeList.emplace_back(__Instruction::__POP, "");

            // Save by absolute address
            codeList.emplace_back(__Instruction::__AST, "");
        }
    }
    // Global variable
    else
    {
        codeList.emplace_back(__Instruction::__LDC, to_string(__symbolTable.at("__GLOBAL__").at(root->__subList[0]->__tokenStr).first));

        // Scalar
        if (root->__subList.size() == 1)
        {
            codeList.emplace_back(__Instruction::__AST, "");
        }
        // Array
        else
        {
            auto exprCodeList = __generateExprCode(root->__subList[1], curFuncName);

            // Absolute get the (start) pointer (is already an absolute address)
            codeList.emplace_back(__Instruction::__ALD, "");
            codeList.emplace_back(__Instruction::__PUSH, "");

            codeList.insert(codeList.end(), exprCodeList.begin(), exprCodeList.end());

            // Pointer[Index] (Pointer + Index)
            codeList.emplace_back(__Instruction::__ADD, "");
            codeList.emplace_back(__Instruction::__POP, "");

            // Save by absolute address
            codeList.emplace_back(__Instruction::__AST, "");
        }
    }

    codeList.emplace_back(__Instruction::__POP, "");

    return codeList;
}

观察__generateAssignCode函数的调用时机可知:这个函数的root,实际上也是一个Var节点,而在调用这个函数时,AX中已经装载了需要被赋值的值。所以我们首先要做的就是:将AX压栈,以保护这个值;接下来,和上一节中一样,我们查找符号表,以确定这个变量是一个局部变量,还是一个全局变量,并通过一条LDC指令装载变量的编号;然后,如果这个变量不是一个数组变量,则我们直接通过一条ST或AST指令,将栈顶值存储进这个变量中,完成代码生成;而如果这个变量是一个数组变量,则同样与上一节类似,我们通过一条LD或ALD指令,装载数组的第一个元素在栈中的索引值;接着,我们通过"PUSH,计算右操作数,ADD,POP,AST"这一系列的指令,完成索引值的偏移与变量的存储动作。最后,我们不能忘记:我们还需要将最开始压入栈中的那个被赋值的值弹出。

至此,变量存取的代码生成器分派函数的实现就全部完成了。接下来,我们将要迎来整个代码生成器,乃至整个编译器实现之旅的重头戏:函数调用的实现。请看下一章:《函数调用的代码生成器分派函数的实现》。

posted @ 2021-02-19 16:50  樱雨楼  阅读(181)  评论(0编辑  收藏  举报