代码生成设置

在上一小节中,我们完成了语法分析器,实现了将源代码转化为内存中的AST,从这一节开始,我们讲学习如何将AST转化为LLVM IR,从此刻起我没就正式接触LLVM了,你将亲眼见证LLVM的简单便捷:跟词法分析器和语法解析器比起来,LLVM IR代码生成部分的开发工作量根本就不值一提。 🙂

在开始生成LLVM IR之前,还有一些准备工作要做。首先,给每个AST类添加一个虚函数Codegen(code generation),用于实现代码生成:

/// ExprAST - Base class for all expression nodes.
class ExprAST {
public:
virtual ~ExprAST() {}
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) {}
virtual Value * codegen();
};
...

每种AST节点的Codegen()方法负责生成该类型AST节点的IR代码及其他必要信息,生成的内容以LLVM Value对象的形式返回。LLVM用Value类表示静态一次性赋值(SSA,Static Single Assignment)的寄存器或SSA值。SSA值最为突出的特点就在于固定不变:SSA值经由对应指令运算得出后便固定下来,直到该指令再次执行之前都不可修改。

除了在ExprAST类体系中添加虚方法以外,还可以利用visitor模式等其他方法来实现代码生成。再次强调,本教程不拘泥于软件工程实践层面的优劣:就当前需求而言,添加虚函数是最简单的方案。

其次,我们还需要一个LogError方法,该方法与语法解析器里用到的报错函数类似,用于报告代码生成过程中发生的错误(例如引用了未经声明的参数):

static LLVMContext TheContext;
static IRBuilder<> Builder(TheContext);
static std::unique_ptr<Module> TheModule;
static std::map<std::string, Value *> NamedValues;

Value *LogErrorV(const char *Str) {
LogError(Str);
return nullptr;
}

上述几个静态变量都是用于完成代码生成的。其中TheModule是LLVM中用于存放代码段中所有函数和全局变量的结构。从某种意义上讲,可以把它当作LLVM IR代码的顶层容器。

Builder是用于简化LLVM指令生成的辅助对象。IRBuilder类模板的实例可用于跟踪当前插入指令的位置,同时还带有用于生成新指令的方法。

TheModule是一个LLVM结构,包含函数和全局变量。 在许多方面,它是LLVM IR用于包含代码的顶级结构。它将拥有我们生成的所有IR的内存,这就是codegen()方法返回原始Value*而不是unique_ptr<Value>的原因。

NamedValues映射表用于记录定义于当前作用域内的变量及与之相对应的LLVM表示(换言之,也就是代码的符号表)。在这一版的Kaleidoscope中,可引用的变量只有函数的参数。因此,在生成函数体的代码时,函数的参数就存放在这张表中。

有了这些,就可以开始进行表达式的代码生成工作了。注意,在生成代码之前必须先设置好Builder对象,指明写入代码的位置。现在,我们姑且假设已经万事俱备,专心生成代码即可。