文章目录
1.概述
在上一章中,我们介绍了如何构建抽象语法树AST。这一章,我们进一步将抽象语法树转换成LLVM IR。此外,我们会简单介绍LLVM的工作原理和使用方法。
注意:本章源码要求LLVM版本大于等于3.7
2.初始化设置
为了支持LLVM IR代码生成,我们需要实现相关的初始化设置。
首先,我们在每一个AST类中定义虚拟代码生成方法,如下所示。
///ExprAST - Base class for all expression nodes.
class ExprAST{
public:
virtual ~ExprAST() = default;
virtual Value *codegen() = 0;
};
/// NumberExprAST - Expression class for numeric literals like "1.0".
class NumberExprAST : public ExprAST{
double Val;
public:
NumberExprAST(double Val) : Val(Val){}
Value *codegen() override;
};
...
codegen()
方法对应AST节点及其所依赖的内容生成IR,返回一个LLVMValue
对象。
Value
是LLVM中用于表示静态单赋值(Static Single Assignment, SSA)寄存器或SSA形式的类。静态单赋值,顾名思义,其要求每个变量只能赋值一次,并且每个变量在使用之前定义。因此,在重新计算之前,变量的值不会发生改变。
其次,我们实现了一个LogError
方法,用于在代码生成期间报告错误,比如:未声明的参数。如下所示:
Value *LogErrorV(const char *Str){
LogError(Str);
return nullptr;
}
此外,我们定义了一系列全局静态变量,用于辅助代码生成,如下所示。
static std::unique_ptr<LLVMContext> TheContext;
static std::unique_ptr<IRBuilder<>> Builder;
static std::unique_ptr<Module> TheModule;
static std::map<std::string, Value *> NamedValues;
TheContext
是一个不透明的对象,它包含许多LLVM核心数据结构,比如类型和常量值表。我们不需要详细了解它,只需要一个单例来传递其所需的API。
Builder
对象是一个辅助对象,用于生成LLVM指令。IRBuilder
类模板的实例能够追踪插入指令的当前位置,并能够创建新指令。
TheModule
是一个包含一系列函数和全局变量的LLVM数据结构。在许多方面,它是LLVM IR用来包含代码的顶层结构。其持有了生成的所有IR的内存,这也是codegen()
方法返回Value*
指针,而不是unique_ptr<Value>
的原因。
NameVlues
用于存储当前作用域内所定义的值及其LLVM表示形式。本质上就是代码的符号表。在Kaleidoscope中,唯一可以被引用的是函数参数。因此,在为函数体生成代码时,函数参数将存储在NamedValues
中。
在介绍了代码生成的基础设置之后,下面我们开始介绍如何为每个表达式生成代码。
3.表达式代码生成
表达式节点的LLVM IR代码生成非常简单。下面,我们将分别进行介绍。
3.1 数值表达式
如下所示,为数值表达式的代码生成方法定义,其创建并返回一个ConstantFP
。事实上,在LLVM IR中,数字常量由ConstantFP
类表示,其内部将数值保存在APFloat
中(APFloat
能够保存任意精度浮点常量)。注意,在LLVM IR中,常量都是唯一且共享的。因此,API通常使用foo::get(...)
的用法,而不是new foo(...)
或foo::Create()
。
Value *NumberExprAST::codegen(){
return ConstantFP::get(TheContext, APFloat(Val));
}
3.2 变量表达式
如下所示,为变量表达式的代码生成方法定义,其仅检查指定变量是否在符号表NameValues
中(如果没有,则引用未知变量)并返回它的值。在目前的定义中,我们在NameValues
中仅存储函数参数。在后续文章节中,我们将在符号表中支持循环归纳变量(Loop Induction Variable)和局部变量(Local Variable)。
Value *VariableExprAST::codegen(){
//Look this variable up in the function.
Value *V = NamedValues[Name];
if(!V)
LogErrorV("Unknown variable name");
return V;
}
3.3 二元表达式
如下所示,为二元表达式的代码生成方法定义,其基本思想是:分别为二元表达式的左部和右部递归生成代码,然后根据操作符的类型分别计算二元表达式的结果。
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");
return Builder.CreateUIToFP(L, Type::getDoubleTy(TheContext), "booltmp");
default:
return LogErrorV("invalid binary operator");
}
}
在上述代码实现中,我们通过LLVMBuilder
类的相关方法来生成代码,其内部知道在何处创建指令,我们要做的就是指定指令的类型(如:“CreateFAdd
”),指定使用的操作数(如:L
和R
),指定生成指令的名称(如:addtmp
)。
关于生成指令的名称,其实只是一个提示。比如说,上述代码中如果发出多个addtmp
变量,LLVM将自动为每个变量提供一个递增的、唯一的数字后缀。指令的名称完全是可选的,它的作用仅仅是提高LLVM IR的可读性。
LLVM指令有着严格规则约束,比如,加法指令的左右操作数的类型必须相同,加法的结果类型必须与操作数类型相同。由于kaleidoscope中所有值都是双精度值类型,这使得add
、sub
、和mul
的代码非常简单。
此外,LLVM规定fcmp
指令必须返回一个i1
值(一个一位整数)。然而,我们定义的kaleidoscope希望该值是0.0或1.0(浮点数)。为了获得这些语义,我们将fcmp
指令与uitofp
指令结合起来。后者通常可以将输入整数转换为浮点值。与此相反,如果我们使用sitofp
指令,那么Kaleidoscope的<
运算符将根据输入值返回0.0和-1.0。
3.3 函数调用表达式
如下所示,为函数调用表达式的代码生成方法定义。
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("Incorrenct # arguments passed");
return::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");
}
在上述方法实现中,在LLVMModule的符号表中查找对应的函数。LLVMModule是JIT阶段存储函数的容器。通过为每个函数指定与用户指定的名称相同的名称,我们可以使用LLVM符号表为我们解析函数名称。
当发生调用函数时,我们递归地对每个入参进行代码生成,并创建一个LLVM函数调用指令。注意,LLVM默认使用C调用规范(c调用规范),允许这些函数调用同事调用标准库函数,如sin
和cos
。
4.函数相关代码生成
函数和原型的代码生成设计一系列的细节处理,这也使得其代码表现不如表达式代码生成实现那么简单优雅。下面,我们来分别介绍函数原型和函数定义的代码生成。
4.1 函数原型
函数原型的代码生成也可以应用于函数定义和外部原型。如下所示,为函数原型的代码生成方法定义。
Function *PrototypeAST::codegen() {
// Make the function type: double(double,double) etc.
std::vector<Type *> Doubles(Args.size(), Type::getDoubleTy(*TheContext));
FunctionType *FT = FunctionType::get(Type::getDoubleTy(*TheContext), Doubles, false);
Function *F = Function::Create(FT, Function::ExternalLinkage, Name, TheModule.get());
// Set names for all arguments.
unsigned Idx = 0;
for (auto &Arg : F->args())
Arg.setName(Args[Idx++]);
return F;
}
PrototypeAST::codegen
函数返回的类型是Function *
,而不是Value
。本质上,原型用于描述函数的外部接口,而不是表达式计算的值,因此其返回代码生成时对应了LLVM的函数,即Function *
类型。
FunctionType::get
函数为原型创建函数类型FunctionType
。由于kaleidoscope中所有的函数参数都是双精度类型,因此在调用Function::get
之前,我们根据参数的数量Args.size()
创建了一个包含N
个LLVM双精度类型的向量。然后,使用FunctionType::get
方法创建一个函数类型,该函数类型接收N
个双精度类型的值作为参数,返回一个双精度类型的值作为结果。
Function::create
函数使用函数类型创建IR函数,其指定函数类型、链接和名称,以及写入的目标模块。**外部链接(External Linkage)**表示函数可以在当前模块外部进行定义,或者可以被模块外部的函数所调用。传入的Name
参数表示用户指定的名称,由于指定了TheModule
,因此该名称会被注册在TheModule
的符号表中。
最后,我们为每个函数参数设置名称。这个步骤非常必要,保持名称一致会使IR更具有更好的可读性,并且允许后续代码直接引用参数名称,而不必在原型的AST中进行查找。
4.2 函数定义
如下所示,为函数定义的代码生成方法定义。
Function *FunctionAST::codegen() {
// First, check for an existing function from a previous 'extern' declaration.
Function *TheFunction = TheModule->getFunction(Proto->getName());
if (!TheFunction)
TheFunction = Proto->codegen();
if (!TheFunction)
return nullptr;
// Create a new basic block to start insertion into.
BasicBlock *BB = BasicBlock::Create(*TheContext, "entry", TheFunction);
Builder->SetInsertPoint(BB);
// Record the function arguments in the NamedValues map.
NamedValues.clear();
for (auto &Arg : TheFunction->args())
NamedValues[std::string(Arg.getName())] = &Arg;
if (Value *RetVal = Body->codegen()) {
// Finish off the function.
Builder->CreateRet(RetVal);
// Validate the generated code, checking for consistency.
verifyFunction(*TheFunction);
return TheFunction;
}
// Error reading body, remove function.
TheFunction->eraseFromParent();
return nullptr;
}
对于函数定义,我们首先在模块的符号表中查找该函数已有的版本。如果Module::getFunction
返回null
则表示不存在,因此我们将通过Proto->codegen
来生成。
然后,我们对Builder
进行设置,具体分为了两个步骤:
- 通过
BasicBlock::Create
函数创建一个基本块(Basic Block),基本块随后被插入到TheFunction
中。 - 通过
Builder->SetInsertPoint
函数设置基本块的末尾为指令的插入位置。LLVM中的基本块是定义控制流图(Control Flow Graph)中的函数的重要组成部分。由于我们没有任何的控制流语句,我们的函数此时只包含一个基本块。
随后,我们将函数参数添加到NameValues
符号表中,以便VariableExprAST
节点进行访问。
当设置了代码插入点并注册了NamedValues
符号表后,我们开始对函数体执行codegen()
方法。如果没有发生错误,我们会进行创建一个LLVMret
指令,表示函数的终止。当函数构建完成后,我们嗲用LLVM的verifyFunction
函数对生成的代码进行各种一致性检查,以确保我们的编译器是否一切正常。verifyFunction
函数非常重要,他能够检测出多个bug。检测完毕且无错误,我们返回构造的函数。
最后的实现逻辑是错误处理。当错误发生时,我们仅通过调用eraseFromParent
函数来删除过程中生成的函数。这样能够允许用户重新定义他们之前输入的错误函数,如果我们不删除它,它将存在于符号表中,并且无法重新定义。
5. 驱动适配器
为了进行测试,我们对驱动器进行了简单的配置,将对codegen
的嗲用插入到HandleDefinition
、HandleExtern
等函数,并在其内部打印LLVM IR。
对于顶层二元表达式,LLVMIR的代码生成结果如下所示。我们可以看到解析器如何将顶层表达式转换为匿名函数的,这对于我们后续支持JIT是非常有用的。此外,我们可以看到生成的IR是逐字翻译的,除了常量合并,没有做其他任何优化。在后续的文章中,我们会显式地进行优化。
ready> 4+5;
Read top-level expression:
define double @__anon_expr() {
entry:
ret double 9.000000e+00
}
对于简单的函数定义,如下所示,分别是LLVM IR的代码生成结果,以及生成过程的示意图。
ready> def foo(a b) a*a + 2*a*b + b*b;
Read function definition:
define double @foo(double %a, double %b) {
entry:
%multmp = fmul double %a, %a
%multmp1 = fmul double 2.000000e+00, %a
%multmp2 = fmul double %multmp1, %b
%addtmp = fadd double %multmp, %multmp2
%multmp3 = fmul double %b, %b
%addtmp4 = fadd double %addtmp, %multmp3
ret double %addtmp4
}
对于简单的函数调用,LLVM IR的代码生成结果如下所示。当我们调用bar
函数时,其会消耗比较长的时间执行完毕。后续我们添加条件控制流来优化递归。
ready> def bar(a) foo(a, 4.0) + bar(31337);
Read function definition:
define double @bar(double %a) {
entry:
%calltmp = call double @foo(double %a, double 4.000000e+00)
%calltmp1 = call double @bar(double 3.133700e+04)
%addtmp = fadd double %calltmp, %calltmp1
ret double %addtmp
}
如下所示,为声明外部cos
函数以及调用cos
函数的IR代码
ready> extern cos(x);
Read extern:
declare double @cos(double)
ready> cos(1.234);
Read top-level expression:
define double @__anon_expr() {
entry:
%calltmp = call double @cos(double 1.234000e+00)
ret double %calltmp
}
当我们退出测试进程时(MacOS 系统,输入 cmd+D),其会打印生成的 Module 的所有 IR 代码。我们可以看到很多方法相互引用,如下所示:
ready> ^D
; ModuleID = 'my cool jit'
source_filename = "my cool jit"
define double @foo(double %a, double %b) {
entry:
%multmp = fmul double %a, %a
%multmp1 = fmul double 2.000000e+00, %a
%multmp2 = fmul double %multmp1, %b
%addtmp = fadd double %multmp, %multmp2
%multmp3 = fmul double %b, %b
%addtmp4 = fadd double %addtmp, %multmp3
ret double %addtmp4
}
define double @bar(double %a) {
entry:
%calltmp = call double @foo(double %a, double 4.000000e+00)
%calltmp1 = call double @bar(double 3.133700e+04)
%addtmp = fadd double %calltmp, %calltmp1
ret double %addtmp
}
declare double @cos(double)
6. 总结
本文,我们为解析器支持了 LLVM IR 代码生成功能,后续在此基础上,为 Kaleidoscope 支持 JIT 代码生成以及优化器。
7. 参考
Kaleidoscope: Code generation to LLVM IR