函数的代码生成

函数原型和函数的代码生成比较繁琐,相关代码不及表达式的代码生成来得优雅,不过却刚好可以用于演示一些重要概念。首先,我们来看看函数原型的代码生成过程:函数定义和外部函数声明都依赖于它。这部分代码一开始是这样的:

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());

短短几行暗藏玄机。首先需要注意的是该函数的返回值类型是Function*而不是Value*。“函数原型”描述的是函数的对外接口(而不是某表达式计算出的值),返回代码生成过程中与之相对应的LLVM Function自然也合情合理。

FunctionType::get调用用于为给定的函数原型创建对应的FunctionType对象。在Kaleidoscope中,函数的参数全部都是double,因此第一行创建了一个包含“N”个LLVM doublevector。随后,FunctionType::get方法以这“N”个double为参数类型、以单个double为返回值类型,创建出一个参数个数不可变(最后一个参数false就是这个意思)的函数类型。注意,和常数一样,LLVM中的类型对象也是单例,应该用“get”而不是“new”来获取。

最后一行实际上创建的是与该函数原型相对应的函数。其中包含了类型、链接方式和函数名等信息,还指定了该函数待插入的模块。ExternalLinkage表示该函数可能定义于当前模块之外,且/或可以被当前模块之外的函数调用。Name是用户指定的函数名:如上述代码中的调用所示,既然将函数定义在“TheModule”内,函数名自然也注册在“TheModule”的符号表内。

// Set names for all arguments.
unsigned Idx = 0;
for (auto &Arg : F->args())
Arg.setName(Args[Idx++]);

return F;

最后,我们根据Prototype中给出的名称设置每个函数参数的名称。
此步骤并非严格必要,但保持名称一致会使IR更具可读性,并允许后续代码直接引用其名称的参数,而不必在Prototype AST中查找它们。

在这一点上,我们有一个没有身体的功能原型。 这是LLVM
IR表示函数声明的方式。 对于Kaleidoscope中的外部陈述,这是我们需要的。 但是对于函数定义,我们需要codegen并附加一个函数体。

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;

if (!TheFunction->empty())
return (Function*)LogErrorV("Function cannot be redefined.");

对于函数定义,我们首先在TheModule的符号表中搜索此函数的现有版本,以防已经使用extern语句创建了一个。
如果Module::getFunction返回null,则不存在先前版本,因此我们将从Prototype中编译一个。在任何一种情况下,我们都要在开始之前断言函数是空的(即还没有正文)。

// 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[Arg.getName()] = &Arg;

现在该开始设置Builder对象了。第一行新建了一个名为entry的基本块对象,稍后该对象将被插入TheFunction。第二行告诉Builder,后续的新指令应该插至刚刚新建的基本块的末尾处。LLVM基本块是用于定义控制流图(Control Flow Graph)的重要部件。当前我们还不涉及到控制流,所以所有的函数都只有一个基本块。接下来,我们将函数参数添加到NamedValues映射(首先清除它之后),以便VariableExprAST节点可以访问它们。

if (Value *RetVal = Body->codegen()) {
// Finish off the function.
Builder.CreateRet(RetVal);

// Validate the generated code, checking for consistency.
verifyFunction(*TheFunction);

return TheFunction;
}

一旦设置了插入点并填充了NamedValues映射,我们就会调用codegen()方法来获取函数的根表达式。
如果没有发生错误,则会发出代码以将表达式计算到条目块中并返回计算的值。
假设没有错误,我们然后创建一个LLVM ret指令,完成该功能。
构建函数后,我们调用LLVM提供的verifyFunction
此函数对生成的代码执行各种一致性检查,以确定我们的编译器是否正在执行所有操作。
使用它很重要:它可以捕获很多错误。 功能完成并验证后,我们将其返回。

  // Error reading body, remove function.
  TheFunction->eraseFromParent();
  return nullptr;
}

这里留下的唯一一件事是处理错误案例。
为简单起见,我们仅通过删除使用eraseFromParent方法生成的函数来处理此问题。
这允许用户重新定义之前错误输入的函数:如果我们没有删除它,它将存在于符号表中,带有正文,从而阻止将来重新定义。

但是,他的代码确实存在错误:如果FunctionAST::codegen()方法找到现有的IR函数,则它不会根据定义自己的原型验证其签名。
这意味着较早的extern声明将优先于函数定义的签名,这可能导致codegen失败,例如,如果函数参数的名称不同。
有很多方法可以解决这个问题,看看你能想出什么! 这是一个测试用例:

extern foo(a);     # ok, defines foo.
def foo(b) b;      # Error: Unknown variable name. (decl using 'a' takes precedence).