简介
欢迎来到“用LLVM实现语言”教程的第5章。第1-4部分描述了简单Kaleidoscope语言的实现,包括对生成LLVM IR的支持,然后是优化和JIT编译器。不幸的是,Kaleidoscope基本上是无用的:它除了调用和返回之外没有控制流。这意味着您不能在代码中使用条件分支,从而极大地限制了它的功能。在这一节“构建编译器”中,我们将扩展Kaleidoscope,使其具有if/then/else表达式和一个简单的for循环。
5.2. If/Then/Else
扩展Kaleidoscope来支持if/then/else是非常简单的。它基本上需要在词法分析器、解析器、AST和LLVM代码发射器中添加对这个“新”概念的支持。这个例子很好,因为它显示了随着时间的推移“发展”一门语言是多么容易,随着新思想的发现而逐渐扩展它。
在我们开始“如何”添加这个扩展之前,让我们谈谈我们想要的“什么”。基本的想法是我们希望能够写出这样的东西:
def fib(x)
if x < 3 then
1
else
fib(x-1)+fib(x-2);
在Kaleidoscope中,每个结构都是一个表达式:没有语句。因此,if/then/else表达式需要像其他表达式一样返回一个值。由于我们使用的主要是函数形式,我们将让它评估它的条件,然后根据条件的解决方式返回‘ then ’或‘ else ’值。这和C " ?:“表达式。
if/then/else表达式的语义是,它将条件计算为布尔相等值:0.0被认为是假,其他所有内容都被认为是真。如果条件为真,则计算并返回第一个子表达式,如果条件为假,则计算并返回第二个子表达式。因为Kaleidoscope会产生副作用,所以确定这种行为是很重要的。
现在我们知道了我们“想要”什么,让我们把它分解成它的组成部分。
5.2.1.为If/Then/Else扩展词法分析
词法分析器扩展很简单。首先,为相关的令牌添加新的枚举值:
// control
tok_if = -6,
tok_then = -7,
tok_else = -8,
一旦有了这些,我们就可以在词法分析器中识别新的关键字。这是非常简单的东西:
...
if (IdentifierStr == "def")
return tok_def;
if (IdentifierStr == "extern")
return tok_extern;
if (IdentifierStr == "if")
return tok_if;
if (IdentifierStr == "then")
return tok_then;
if (IdentifierStr == "else")
return tok_else;
return tok_identifier;
5.2.2. 为If/Then/Else扩展AST
为了表示新的表达式,我们为它添加一个新的AST节点:
/// IfExprAST - Expression class for if/then/else.
class IfExprAST : public ExprAST {
std::unique_ptr<ExprAST> Cond, Then, Else;
public:
IfExprAST(std::unique_ptr<ExprAST> Cond, std::unique_ptr<ExprAST> Then,
std::unique_ptr<ExprAST> Else)
: Cond(std::move(Cond)), Then(std::move(Then)), Else(std::move(Else)) {}
Value *codegen() override;
};
AST节点只有指向不同子表达式的指针。
5.2.3. 为If/Then/Else扩展Parser
既然我们有了来自词法分析器的相关令牌,并且有了要构建的AST节点,那么我们的Parser逻辑就相对简单了。首先我们定义一个新的解析函数:
/// ifexpr ::= 'if' expression 'then' expression 'else' expression
static std::unique_ptr<ExprAST> ParseIfExpr() {
getNextToken(); // eat the if.
// condition.
auto Cond = ParseExpression();
if (!Cond)
return nullptr;
if (CurTok != tok_then)
return LogError("expected then");
getNextToken(); // eat the then
auto Then = ParseExpression();
if (!Then)
return nullptr;
if (CurTok != tok_else)
return LogError("expected else");
getNextToken();
auto Else = ParseExpression();
if (!Else)
return nullptr;
return std::make_unique<IfExprAST>(std::move(Cond), std::move(Then),
std::move(Else));
}
接下来,我们把它连接成一个主表达式:
static std::unique_ptr<ExprAST> ParsePrimary() {
switch (CurTok) {
default:
return LogError("unknown token when expecting an expression");
case tok_identifier:
return ParseIdentifierExpr();
case tok_number:
return ParseNumberExpr();
case '(':
return ParseParenExpr();
case tok_if:
return ParseIfExpr();
}
}
5.2.4. 为If/Then/Else生成LLVM IR
现在我们已经完成了AST的解析和构建,最后一部分是添加LLVM代码生成支持。这是if/then/else示例中最有趣的部分,因为从这里开始引入新概念。上面所有的代码在前面的章节中都有详细的描述。
为了我们想要生成的代码,让我们看一个简单的示例。考虑:
extern foo();
extern bar();
def baz(x) if x then foo() else bar();
如果你禁用优化,你将(很快)从Kaleidoscope中得到的代码看起来像这样的
declare double @foo()
declare double @bar()
define double @baz(double %x) {
entry:
%ifcond = fcmp one double %x, 0.000000e+00
br i1 %ifcond, label %then, label %else
then: ; preds = %entry
%calltmp = call double @foo()
br label %ifcont
else: ; preds = %entry
%calltmp1 = call double @bar()
br label %ifcont
ifcont: ; preds = %else, %then
%iftmp = phi double [ %calltmp, %then ], [ %calltmp1, %else ]
ret double %iftmp
}
为了可视化控制流图,您可以使用LLVM'opt'工具的一个漂亮功能。如果你把这个LLVM IR放进“t.ll”。然后运行“llvm-as < t.l | opt -passes=view-cfg”,会弹出一个窗口,你会看到下面的图:
另一种方法是调用“F->viewCFG()”或“F->viewCFGOnly()”(其中F是一个“函数*”),通过插入实际调用到代码中并重新编译或通过在调试器中调用这些。LLVM有许多很好的特性来可视化各种图形。
回到生成的代码,它相当简单:入口块计算条件表达式(在本例中为“x”),并将结果与“fcmp one”指令(“one”是“有序且不相等”)进行比较。根据该表达式的结果,代码跳转到“then”或“else”块,其中包含true/false情况的表达式。
一旦then/else代码块完成执行,它们都会分支回'ifcont'代码块来执行if/then/else之后的代码。在这种情况下,剩下唯一要做的就是返回到函数的调用者。那么问题就变成了:代码如何知道返回哪个表达式
这个问题的答案涉及到一个重要的SSA运算:Phi操作。如果您不熟悉SSA,维基百科的文章是一个很好的介绍,在您最喜欢的搜索引擎上还有各种其他介绍。简而言之,Phi运算的“执行”需要“记住”哪个块控制来自。运算取与输入控制块对应的值。在这种情况下,如果控制来自"then"块,它将获取" calltmp"的值。如果控制来自"else",它获取"calltmp1"的值。
在这一点上,你可能开始想“哦,不!这意味着为了使用LLVM,我的简单而优雅的前端必须开始生成SSA表单!”幸运的是,情况并非如此,我们强烈建议不要在前端实现SSA构造算法,除非有非常好的理由这样做。在实践中,有两种类型的值浮动在你的普通命令式编程语言的代码中,可能需要Phi节点:
- 涉及用户变量的代码:x = 1;X = X + 1;
- 在AST结构中隐式的值,例如本例中的Phi节点。
在本教程的第7章("可变变量")中,我们将深入讨论#1。现在,你不需要特别安全局来处理这个case。对于第二条,你可以选择使用我们在第一条中描述的技术,或者你可以直接插入节点,如果方便的话。在这种情况下,生成节点很容易,所以我们选择直接生成。
好了,动机和概述说得够多了,让我们来生成代码吧!
5.2.5. 为 If/Then/Else 生成代码
为了生成相应的代码,我们实现IfExprAST的codegen方法:
Value *IfExprAST::codegen() {
Value *CondV = Cond->codegen();
if (!CondV)
return nullptr;
// Convert condition to a bool by comparing non-equal to 0.0.
CondV = Builder->CreateFCmpONE(
CondV, ConstantFP::get(*TheContext, APFloat(0.0)), "ifcond");
这段代码很简单,与我们之前看到的类似。我们为条件发出表达式,然后将该值与零进行比较,以获得一个1位(bool)值的真值。
Function *TheFunction = Builder->GetInsertBlock()->getParent();
// Create blocks for the then and else cases. Insert the 'then' block at the
// end of the function.
BasicBlock *ThenBB =
BasicBlock::Create(*TheContext, "then", TheFunction);
BasicBlock *ElseBB = BasicBlock::Create(*TheContext, "else");
BasicBlock *MergeBB = BasicBlock::Create(*TheContext, "ifcont");
Builder->CreateCondBr(CondV, ThenBB, ElseBB);
这段代码创建了与if/then/else语句相关的基本块,并直接对应于上面示例中的块。第一行获取正在构建的当前Function对象。它通过向构建器询问当前BasicBlock,并向该块的“父”块询问(当前嵌入的函数)来获得此值。
一旦有了这些,它就创建了三个块。注意,它将“TheFunction”传递给了“then”块的构造函数。这将导致构造函数自动将新块插入到指定函数的末尾。另外两个块已经创建,但还没有插入到函数中。
一旦创建了块,我们就可以发出在它们之间进行选择的条件分支。注意,创建新的块并不隐式地影响IRBuilder,因此它仍然插入到条件进入的块中。还要注意,它正在创建“then”块和“else”块的分支,即使“else”块还没有插入到函数中。这都没问题:这是LLVM支持前向引用的标准方式。
// Emit then value.
Builder->SetInsertPoint(ThenBB);
Value *ThenV = Then->codegen();
if (!ThenV)
return nullptr;
Builder->CreateBr(MergeBB);
// Codegen of 'Then' can change the current block, update ThenBB for the PHI.
ThenBB = Builder->GetInsertBlock();
在插入条件分支之后,我们移动构建器开始插入到"then"块中。严格地说,这个调用将插入点移动到指定块的末尾。然而,由于"then"块是空的,它也从块的开头插入开始。:)
一旦插入点被设置,我们就从AST递归地编码"then"表达式。为了完成"then"块,我们创建了merge块的无条件分支。LLVM IR的一个有趣(而且非常重要)的方面是,它要求所有基本块都用返回或分支等控制流指令"终止"。这意味着所有的控制流,包括fall throughs都必须在LLVM IR中明确。如果违反此规则,验证器将发出错误。
这里的最后一行非常微妙,但非常重要。基本问题是,当我们在合并块中创建Phi节点时,我们需要设置块/值对,以指示Phi将如何工作。重要的是,Phi节点期望在CFG中每个块的前身都有一个条目。那么,当我们将其设置为5行以上的ThenBB时,为什么我们得到的是当前块呢?问题是"Then"表达式本身可能会改变Builder发出的if块,例如,它包含一个嵌套的"if/ Then /else"表达式。因为递归地调用codegen()可以任意地改变当前块的概念,所以我们需要获得将设置Phi节点的代码的最新值。
// Emit else block.
TheFunction->insert(TheFunction->end(), ElseBB);
Builder->SetInsertPoint(ElseBB);
Value *ElseV = Else->codegen();
if (!ElseV)
return nullptr;
Builder->CreateBr(MergeBB);
// codegen of 'Else' can change the current block, update ElseBB for the PHI.
ElseBB = Builder->GetInsertBlock();
'else'块的代码生成基本上与'then'块的代码生成相同。唯一显著的区别是第一行,它将' else'块添加到函数中。回想一下,前面创建了'else'块,但没有添加到函数中。现在'then '和'else'块已经发出,我们可以完成合并代码:
// Emit merge block.
TheFunction->insert(TheFunction->end(), MergeBB);
Builder->SetInsertPoint(MergeBB);
PHINode *PN =
Builder->CreatePHI(Type::getDoubleTy(*TheContext), 2, "iftmp");
PN->addIncoming(ThenV, ThenBB);
PN->addIncoming(ElseV, ElseBB);
return PN;
}
这里的前两行现在很熟悉了:第一行将“merge”块添加到Function对象中(它之前是浮动的,就像上面的else块一样)。第二个更改插入点,以便新创建的代码将进入“merge”块。一旦完成,我们需要创建PHI节点并为PHI设置块/值对。
最后,CodeGen函数返回phi节点,作为if/then/else表达式计算的值。在上面的示例中,这个返回值将被输入到顶层函数的代码中,顶层函数将创建返回指令
总的来说,我们现在有能力在Kaleidoscope中执行条件代码。有了这个扩展,Kaleidoscope是一个相当完整的语言,可以计算各种各样的数字函数。接下来,我们将添加另一个在非函数式语言中很常见的有用表达式。
5.3. for循环表达式
既然我们知道了如何向语言中添加基本的控制流结构,那么我们就有了添加更强大功能的工具。让我们添加一些更激进的表达,一个"for"表达:
extern putchard(char);
def printstar(n)
for i = 1, i < n, 1.0 in
putchard(42); # ascii 42 = '*'
# print 100 '*' characters
printstar(100);
这个表达式定义了一个新变量(在本例中为"i"),它从一个起始值开始迭代,而条件(在本例中为"i < n")为真,递增一个可选的步长值(在本例中为"1.0")。如果省略步长值,则默认为1.0。当循环为true时,它执行其主体表达式。因为我们没有更好的返回值,所以我们将把循环定义为总是返回0.0。将来当我们有可变变量时,它会变得更有用。
和之前一样,让我们谈谈我们需要对Kaleidoscope做些什么来支持它。
5.3.1.为‘for’循环扩展词法分析
词法分析器的扩展和if/then/else是一样的
... in enum Token ...
// control
tok_if = -6, tok_then = -7, tok_else = -8,
tok_for = -9, tok_in = -10
... in gettok ...
if (IdentifierStr == "def")
return tok_def;
if (IdentifierStr == "extern")
return tok_extern;
if (IdentifierStr == "if")
return tok_if;
if (IdentifierStr == "then")
return tok_then;
if (IdentifierStr == "else")
return tok_else;
if (IdentifierStr == "for")
return tok_for;
if (IdentifierStr == "in")
return tok_in;
return tok_identifier;
5.3.2. for循环扩展AST分析
AST节点也同样简单。它基本上归结为捕获节点中的变量名和组成for循环的表达式。
/// ForExprAST - Expression class for for/in.
class ForExprAST : public ExprAST {
std::string VarName;
std::unique_ptr<ExprAST> Start, End, Step, Body;
public:
ForExprAST(const std::string &VarName, std::unique_ptr<ExprAST> Start,
std::unique_ptr<ExprAST> End, std::unique_ptr<ExprAST> Step,
std::unique_ptr<ExprAST> Body)
: VarName(VarName), Start(std::move(Start)), End(std::move(End)),
Step(std::move(Step)), Body(std::move(Body)) {}
Value *codegen() override;
};
5.3.3. 为‘for’ 循环扩展Parser
解析器代码也是相当标准的。这里唯一有趣的事情是处理可选的步长值。解析器代码通过检查第二个逗号是否存在来处理它。如果没有,它将AST节点中的步长值设置为空:
/// forexpr ::= 'for' identifier '=' expr ',' expr (',' expr)? 'in' expression
static std::unique_ptr<ExprAST> ParseForExpr() {
getNextToken(); // eat the for.
if (CurTok != tok_identifier)
return LogError("expected identifier after for");
std::string IdName = IdentifierStr;
getNextToken(); // eat identifier.
if (CurTok != '=')
return LogError("expected '=' after for");
getNextToken(); // eat '='.
auto Start = ParseExpression();
if (!Start)
return nullptr;
if (CurTok != ',')
return LogError("expected ',' after for start value");
getNextToken();
auto End = ParseExpression();
if (!End)
return nullptr;
// The step value is optional.
std::unique_ptr<ExprAST> Step;
if (CurTok == ',') {
getNextToken();
Step = ParseExpression();
if (!Step)
return nullptr;
}
if (CurTok != tok_in)
return LogError("expected 'in' after for");
getNextToken(); // eat 'in'.
auto Body = ParseExpression();
if (!Body)
return nullptr;
return std::make_unique<ForExprAST>(IdName, std::move(Start),
std::move(End), std::move(Step),
std::move(Body));
}
我们再一次把它作为主要的表达:
static std::unique_ptr<ExprAST> ParsePrimary() {
switch (CurTok) {
default:
return LogError("unknown token when expecting an expression");
case tok_identifier:
return ParseIdentifierExpr();
case tok_number:
return ParseNumberExpr();
case '(':
return ParseParenExpr();
case tok_if:
return ParseIfExpr();
case tok_for:
return ParseForExpr();
}
}
}
5.3.4. 为 ‘for’ 循环生成LLVM IR
我们想要为这个东西生成的LLVM IR。通过上面的简单示例,我们得到了这个LLVM IR(注意,为了清晰起见,这个转储是在禁用优化的情况下生成的):
declare double @putchard(double)
define double @printstar(double %n) {
entry:
; initial value = 1.0 (inlined into phi)
br label %loop
loop: ; preds = %loop, %entry
%i = phi double [ 1.000000e+00, %entry ], [ %nextvar, %loop ]
; body
%calltmp = call double @putchard(double 4.200000e+01)
; increment
%nextvar = fadd double %i, 1.000000e+00
; termination test
%cmptmp = fcmp ult double %i, %n
%booltmp = uitofp i1 %cmptmp to double
%loopcond = fcmp one double %booltmp, 0.000000e+00
br i1 %loopcond, label %loop, label %afterloop
afterloop: ; preds = %loop
; loop always returns 0.0
ret double 0.000000e+00
}
这个循环包含我们之前看到的所有相同的结构:一个phi节点、几个表达式和一些基本块。让我们看看这些是如何结合在一起的
5.3.5. 为‘for’循环生成代码
codegen的第一部分非常简单:我们只是输出循环值的开始表达式:
Value *ForExprAST::codegen() {
// Emit the start code first, without 'variable' in scope.
Value *StartVal = Start->codegen();
if (!StartVal)
return nullptr;
解决了这个问题后,下一步是为循环体的开始设置LLVM基本块。在上面的例子中,整个循环体是一个块,但请记住,body代码块本身可以由多个块组成(例如,如果它包含if/then/else或for/ In表达式)。
// Make the new basic block for the loop header, inserting after current
// block.
Function *TheFunction = Builder->GetInsertBlock()->getParent();
BasicBlock *PreheaderBB = Builder->GetInsertBlock();
BasicBlock *LoopBB =
BasicBlock::Create(*TheContext, "loop", TheFunction);
// Insert an explicit fall through from the current block to the LoopBB.
Builder->CreateBr(LoopBB);
这段代码类似于我们看到的if/then/else。因为我们需要它来创建节点,我们记住了进入循环的块。一旦我们有了这个,我们创建开始循环的实际块,并为两个块之间的失败创建一个无条件分支。
// Start insertion in LoopBB.
Builder->SetInsertPoint(LoopBB);
// Start the PHI node with an entry for Start.
PHINode *Variable = Builder->CreatePHI(Type::getDoubleTy(*TheContext),
2, VarName);
Variable->addIncoming(StartVal, PreheaderBB);
现在循环的“preheader”已经设置好了,我们切换到为循环体发出代码。首先,我们移动插入点并为循环引导变量创建PHI节点。因为我们已经知道起始值的传入值,我们把它加到Phi节点上。注意,Phi最终会为后续获得第二个值,但我们还不能设置它(因为它不存在!)。
// Within the loop, the variable is defined equal to the PHI node. If it
// shadows an existing variable, we have to restore it, so save it now.
Value *OldVal = NamedValues[VarName];
NamedValues[VarName] = Variable;
// Emit the body of the loop. This, like any other expr, can change the
// current BB. Note that we ignore the value computed by the body, but don't
// allow an error.
if (!Body->codegen())
return nullptr;
现在代码开始变得更有趣了。for循环向符号表中引入了一个新变量。这意味着符号表现在既可以包含函数参数,也可以包含循环变量。为了处理这个问题,在对循环体进行编码之前,我们将循环变量添加为其名称的当前值。注意,外部作用域中可能存在同名的变量。这很容易导致错误(如果已经有一个VarName条目,则发出错误并返回null),但我们选择允许遮蔽变量。为了正确处理这件事,我们记住我们在OldVal中隐藏的值(如果没有隐藏的变量,它将为null)。
一旦将循环变量设置到符号表中,代码就会递归地对body生成代码。这允许代码体使用循环变量:对它的任何引用都会在符号表中找到它。
// Emit the step value.
Value *StepVal = nullptr;
if (Step) {
StepVal = Step->codegen();
if (!StepVal)
return nullptr;
} else {
// If not specified, use 1.0.
StepVal = ConstantFP::get(*TheContext, APFloat(1.0));
}
Value *NextVar = Builder->CreateFAdd(Variable, StepVal, "nextvar");
现在body开始生成,我们通过添加步长值来计算迭代变量的下一个值,如果不存在则为1.0。'NextVar'将是下一次循环迭代时循环变量的值。
// Compute the end condition.
Value *EndCond = End->codegen();
if (!EndCond)
return nullptr;
// Convert condition to a bool by comparing non-equal to 0.0.
EndCond = Builder->CreateFCmpONE(
EndCond, ConstantFP::get(*TheContext, APFloat(0.0)), "loopcond");
最后,我们计算循环的退出值,以确定是否应该退出循环。这反映了if/then/else语句的条件计算。
// Create the "after loop" block and insert it.
BasicBlock *LoopEndBB = Builder->GetInsertBlock();
BasicBlock *AfterBB =
BasicBlock::Create(*TheContext, "afterloop", TheFunction);
// Insert the conditional branch into the end of LoopEndBB.
Builder->CreateCondBr(EndCond, LoopBB, AfterBB);
// Any new code will be inserted in AfterBB.
Builder->SetInsertPoint(AfterBB);
循环体的代码完成后,我们只需要完成它的控制流。此代码记住结束块(用于phi节点),然后为循环出口("后循环")创建块。根据退出条件的值,它创建一个条件分支,在再次执行循环和退出循环之间进行选择。任何未来的代码都在"afterloop"块中开始,因此它为其设置插入位置。
// Add a new entry to the PHI node for the backedge.
Variable->addIncoming(NextVar, LoopEndBB);
// Restore the unshadowed variable.
if (OldVal)
NamedValues[VarName] = OldVal;
else
NamedValues.erase(VarName);
// for expr always returns 0.0.
return Constant::getNullValue(Type::getDoubleTy(*TheContext));
}
最后的代码处理各种清理:现在我们有了"NextVar"值,我们可以将传入值添加到循环PHI节点。之后,从符号表中删除循环变量,这样它就不在for循环后的作用域中了。最后,for循环的代码生成总是返回0.0,这就是我们从ForExprAST::codegen()返回的结果。
至此,我们结束了本教程的“向Kaleidoscope添加控制流”一章。在本章中,我们添加了两个控制流结构,并使用它们来激发LLVM IR的几个方面,这些方面对于前端实现者来说是很重要的。在我们的传奇故事的下一章中,我们将为我们的语言添加用户定义的操作符