表达式的代码生成

为表达式节点生成LLVM代码的过程十分简单明了:连带注释只需区区45行代码便足以搞定全部四种表达式节点。首先是数值常量:

Value *NumberExprAST::codegen() {
return ConstantFP::get(TheContext, APFloat(Val));
}

LLVM IR中的数值常量是由ConstantFP类表示的。在其内部,具体数值由APFloat(Arbitrary Precision Float,可用于存储任意精度的浮点数常量)表示。这段代码说白了就是新建并返回了一个ConstantFP对象。值得注意的是,在LLVM IR内部,常量都只有一份,并且是共享的。因此,API往往会采用foo:get(...)的形式而不是new foo(...)foo::Create(...)

Value *VariableExprAST::codegen() {
// Look this variable up in the function.
Value * V = NamedValues[Name];
if (!V)
LogErrorV("Unknown variable name");
return V;
}

在LLVM中引用变量也很简单。在简化版的Kaleidoscope中,我们大可假设被引用的变量已经在某处被定义并赋值。实际上,位于NamedValues映射表中的变量只可能是函数的调用参数。这段代码首先确认给定的变量名是否存在于映射表中(如果不存在,就说明引用了未定义的变量)然后返回该变量的值。在后续章节中,我们还会对语言做进一步的扩展,让符号表支持循环归纳变量和局部变量。

Value *BinaryExprAST::codegen() {
Value *L = LHS->codegen();
Value *R = RHS->codegen();
if (!L || !R)
return nullptr;

switch (Op) {
case '+':
return Builder.CreateFAdd(L, R, "addtmp");
case '-':
return Builder.CreateFSub(L, R, "subtmp");
case '*':
return Builder.CreateFMul(L, R, "multmp");
case '<':
L = Builder.CreateFCmpULT(L, R, "cmptmp");
// Convert bool 0/1 to double 0.0 or 1.0
return Builder.CreateUIToFP(L, Type::getDoubleTy(TheContext),
"booltmp");
default:
return LogErrorV("invalid binary operator");
}
}

二元运算符的处理就比较有意思了。其基本思想是递归地生成代码,先处理表达式的左侧,再处理表达式的右侧,最后计算整个二元表达式的值。上述代码就opcode的取值用了一个简单的switch语句,从而为各种二元运算符创建出相应的LLVM指令。

在上面的例子中,LLVM的Builder类逐渐开始凸显出自身的价值。你只需想清楚该用哪些操作数(即此处的LR)生成哪条指令(通过调用CreateFAdd等方法)即可,至于新指令该插入到什么位置,交给IRBuilder就可以了。此外,如果需要,你还可以给生成的指令指定一个名字。

LLVM的优点之一在于此处的指令名只是一个提示。举个例子,假设上述代码生成了多条addtmp指令,LLVM会自动给每条指令的名字追加一个自增的唯一数字后缀。指令的local value name完全是可选的,但它能大大提升dump出来的IR代码的可读性。

LLVM指令遵循严格的约束:例如,add指令的LeftRight操作数必须同属一个类型,结果的类型则必须与操作数的类型相容。由于Kaleidoscope中的值都是双精度浮点数,addsubmul指令的代码得以大大简化。

然而,LLVM要求fcmp指令的返回值类型必须是i1(单比特整数)。问题在于Kaleidoscope只能接受0.01.0。为了弥合语义上的差异,我们给fcmp指令配上一条uitofp指令。这条指令会将输入的整数视作无符号数,并将之转换成浮点数。相应地,如果用的是sitofp指令,Kaleidoscope的<运算符将视输入的不同而返回0.0-1.0

Value *CallExprAST::codegen() {
// Look up the name in the global module table.
Function *CalleeF = TheModule->getFunction(Callee);
if (!CalleeF)
return LogErrorV("Unknown function referenced");

// If argument mismatch error.
if (CalleeF->arg_size() != Args.size())
return LogErrorV("Incorrect # arguments passed");

std::vector<Value *> ArgsV;
for (unsigned i = 0, e = Args.size(); i != e; ++i) {
ArgsV.push_back(Args[i]->codegen());
if (!ArgsV.back())
return nullptr;
}

return Builder.CreateCall(CalleeF, ArgsV, "calltmp");
}

函数调用的代码生成非常直截了当。上述代码开头的几行是在LLVM Module的符号表中查找函数名。如前文所述,LLVM Module是个容器,待处理的函数全都在里面。只要保证各函数的名字与用户指定的函数名一致,我们就可以利用LLVM的符号表替我们完成函数名的解析。

拿到待调用的函数之后,就递归地生成传入的各个参数的代码,并创建一条LLVM call指令。注意,LLVM默认采用本地的C调用规范,这样以来,就可以毫不费力地调用标准库中的sincos等函数了。

Kaleidoscope中的四种基本表达式的代码生成就介绍完了。尽情地添枝加叶去吧。去试试LLVM语言参考上的各种千奇百怪的指令,以当前的基本框架为基础,支持这些指令易如反掌。