结合文法,相信很容易看到语法树的代码。但接下来的代码,因为没有类似文法一样的东西指导我们,代码量多起来就会迷糊。我们要着眼于数据,分析语法树中的数据流向了哪里,这样可以在大量的代码中抓住要点。
检查语法树,构造出ty这个数据结构。
CheckDeclarationSpecifiers(func->specs);
CheckDeclarator(func->dec);
ty = DeriveType(func->dec->tyDrvList, func->specs->ty,&func->coord);
在函数CheckDeclarator中,会将函数的参数放在funcDec->sig->params中。
如果函数之前没被定义过,就添加到符号表中
func->fsym = (FunctionSymbol)AddFunction(func->dec->id, ty, sclass,&func->coord);
接下来的一堆代码
RestoreParameterListTable();
{
Vector v= ((FunctionType)ty)->sig->params;
FOR_EACH_ITEM(Parameter, param, v)
AddVariable(param->id, param->ty, param->reg ? TK_REGISTER : TK_AUTO,&func->coord);
ENDFOR
FSYM->locals = NULL;
FSYM->lastv = &FSYM->locals;
}
这是处理那种返回值比较复杂的情况用的,例如void (*h(int a1,int a2))(void){} 这种返回函数指针的情况。暂且忽略。
代码
CheckCompoundStatement(func->stmt);
语句检查将局部作用域里面的声明加入对应作用域的符号表,然后检查语句是否有语义错误。
我们发现语义检查除了检查语义错误外,最大的作用是将声明加入到符号表中。
params 记录了函数形参,locals 用于记录局部变量,valNumTable[16]则是一个哈希表,用于快速查找形如“a+b”的公共子表达式。
由于同一个函数名或变量名可以在不同作用域中被多次声明,我们需要把同一个符号加入到不同的符号表中,为此,我们引入结构体 bucketLinker,我们往Table中添加的是 bucketLinker 对象,该对象的 sym 域指向了对应的符号。
每一个作用域都有一个符号表,这些符号表通过outer相连:
声明部分在语义检查过程中添加到符号表中了。
语句和表达式将会变成由BBlock组成的控制流图,BBlock内部的语句都是顺序执行的,而分支、跳转都是在BBlock之间来回跳转。
bblock中有一个链表,里面都是irinst结构体,里面有对应的指令的三地址码。
临时变量也是在这一步创建的。
分支语句的跳转指令是在这一步生成的。
中间代码与汇编相比,还有哪些差别呢?
那么为每条伪指令都写一个函数,逐条翻译成汇编指令吗?大可不必。因为很多指令有相同的地方,我们可分为15类,每一类用一个函数来处理即可。
EmitMove
EmitAssign
EmitBranch
EmitJump
EmitIndirectJump
EmitInc
EmitDec
EmitAddress
EmitDeref
EmitCast
EmitIndirectMove
EmitCall
EmitReturn
大多数比较简单,但EmitCall、EmitReturn相对比较复杂,因为函数调用与返回涉及寄存器的保存和恢复。这在后面单独介绍。