为表达式节点生成LLVM代码的过程十分简单明了:连带注释只需区区45行代码便足以搞定全部四种表达式节点。首先是数值常量:
Value *NumberExprAST::codegen() { |
LLVM IR中的数值常量是由ConstantFP
类表示的。在其内部,具体数值由APFloat
(Arbitrary Precision Float,可用于存储任意精度的浮点数常量)表示。这段代码说白了就是新建并返回了一个ConstantFP
对象。值得注意的是,在LLVM IR内部,常量都只有一份,并且是共享的。因此,API往往会采用foo:get(...)
的形式而不是new foo(...)
或foo::Create(...)
。
Value *VariableExprAST::codegen() { |
在LLVM中引用变量也很简单。在简化版的Kaleidoscope中,我们大可假设被引用的变量已经在某处被定义并赋值。实际上,位于NamedValues
映射表中的变量只可能是函数的调用参数。这段代码首先确认给定的变量名是否存在于映射表中(如果不存在,就说明引用了未定义的变量)然后返回该变量的值。在后续章节中,我们还会对语言做进一步的扩展,让符号表支持循环归纳变量和局部变量。
Value *BinaryExprAST::codegen() { |
二元运算符的处理就比较有意思了。其基本思想是递归地生成代码,先处理表达式的左侧,再处理表达式的右侧,最后计算整个二元表达式的值。上述代码就opcode
的取值用了一个简单的switch
语句,从而为各种二元运算符创建出相应的LLVM指令。
在上面的例子中,LLVM的Builder
类逐渐开始凸显出自身的价值。你只需想清楚该用哪些操作数(即此处的L
和R
)生成哪条指令(通过调用CreateFAdd
等方法)即可,至于新指令该插入到什么位置,交给IRBuilder
就可以了。此外,如果需要,你还可以给生成的指令指定一个名字。
LLVM的优点之一在于此处的指令名只是一个提示。举个例子,假设上述代码生成了多条addtmp
指令,LLVM会自动给每条指令的名字追加一个自增的唯一数字后缀。指令的local value name完全是可选的,但它能大大提升dump出来的IR代码的可读性。
LLVM指令遵循严格的约束:例如,add
指令的Left
、Right
操作数必须同属一个类型,结果的类型则必须与操作数的类型相容。由于Kaleidoscope中的值都是双精度浮点数,add
、sub
和mul
指令的代码得以大大简化。
然而,LLVM要求fcmp
指令的返回值类型必须是i1
(单比特整数)。问题在于Kaleidoscope只能接受0.0
或1.0
。为了弥合语义上的差异,我们给fcmp
指令配上一条uitofp
指令。这条指令会将输入的整数视作无符号数,并将之转换成浮点数。相应地,如果用的是sitofp
指令,Kaleidoscope的<
运算符将视输入的不同而返回0.0
或-1.0
。
Value *CallExprAST::codegen() { |
函数调用的代码生成非常直截了当。上述代码开头的几行是在LLVM Module
的符号表中查找函数名。如前文所述,LLVM Module
是个容器,待处理的函数全都在里面。只要保证各函数的名字与用户指定的函数名一致,我们就可以利用LLVM的符号表替我们完成函数名的解析。
拿到待调用的函数之后,就递归地生成传入的各个参数的代码,并创建一条LLVM call
指令。注意,LLVM默认采用本地的C调用规范,这样以来,就可以毫不费力地调用标准库中的sin
、cos
等函数了。
Kaleidoscope中的四种基本表达式的代码生成就介绍完了。尽情地添枝加叶去吧。去试试LLVM语言参考上的各种千奇百怪的指令,以当前的基本框架为基础,支持这些指令易如反掌。