编译器实现之旅——第十四章 变量存取的代码生成器分派函数的实现
在上一章的旅程中,我们已经实现了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
这幅图对我们而言非常重要,其是这一章与接下来两章的旅程中都会用到的示意图。有了这幅图,我们就可以解释很多在前面走过的旅程中无法解释的事情了。请看:
- 在生成符号表的过程中,我们对每个变量进行的看似随意的编号,实际上正对应着这个变量在栈中的位置。对于全局变量而言,符号表中的变量编号就是这个变量在栈中的索引值;而对于局部变量而言,我们可以用"BP - 符号表中的变量编号"得到这个变量在栈中的索引值。这也解释了为什么虚拟机的LD指令需要使用"BP - AX"这样的实现
- 对于数组变量而言,数组变量自身的值是这个数组的第一个元素在栈中的索引值。由此我们可以解释:在生成数组变量的符号表编号时,之所以需要将编号额外的再加上数组长度,正是为了让数组后面的变量的索引值不仅越过数组变量自身,还越过数组的内容
- 我们只需要进行一次访问即可得到任何非数组变量的值;而对于数组变量,我们就必须先得到这个数组的第一个元素在栈中的索引值,再将此索引值进行偏移,最终才能得到数组中某个元素的值
- 显然,这幅图并不是凭空想象画出来的模型图,而是真实存在的,其需要通过代码生成器进行实现。这部分内容我们将在下一章的旅程中进行讨论。这里,我们直接使用这幅图带给我们的结论即可
接下来,就让我们利用上述示意图与结论,来实现变量存取相关的代码生成器分派函数吧。
2. Var节点的分派函数的实现
Var节点用于进行一次变量访问。通过上一节的讨论我们知道,对于一个变量而言,我们需要同时关注两个问题,这两个问题都会影响到变量访问的实现:
- 这个变量是一个局部变量,还是一个全局变量?
- 这个变量是不是一个数组变量?
对于第一个问题,我们的答案显而易见:如果在当前函数的符号表中能够查到这个变量名,就说明这个变量是一个局部变量;否则,这个变量就是一个全局变量。而对于第二个问题,我们的答案也不难想到:如果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中的值可能有两种情况:
- 如果这个变量不是一个数组变量,则AX中的值就是变量的值,代码已经生成完毕
- 反之,如果这个变量是一个数组变量,则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"这一系列的指令,完成索引值的偏移与变量的存储动作。最后,我们不能忘记:我们还需要将最开始压入栈中的那个被赋值的值弹出。
至此,变量存取的代码生成器分派函数的实现就全部完成了。接下来,我们将要迎来整个代码生成器,乃至整个编译器实现之旅的重头戏:函数调用的实现。请看下一章:《函数调用的代码生成器分派函数的实现》。