LLVM控制流程中的If-then-else语句

446 阅读5分钟

控制流语句是用来改变程序执行流程的语句。在这篇文章中,我们将Kaleidoscope扩展到控制流操作,如if-then-else语句。

目录

  1. 介绍
  2. If-Then-Else
  3. Lexer扩展
  4. AST扩展
  5. 解析器扩展
  6. LLVM IR扩展
  7. 代码生成扩展
  8. 总结
  9. 参考资料

先决条件

  1. 实现JIT(Just In Time)编译

简介

在编程语言中,控制流语句被用来改变程序的执行流程。它们包括if-then-else语句和循环,如forwhiledo...while

在前提文章中,我们看到了如何生成LLVM IR,进一步优化代码,并为编程语言构建一个JIT编译器。在这篇文章中,我们将Kaleidoscope扩展到包括if-then-else条件语句。

If-Then-Else

正如我们在以前的文章中所看到的,要在我们的编程语言中实现一个新的功能,我们需要在词典、分析器、AST和LLVM IR中添加对它的支持,在这种情况下,我们将添加对if-then-else条件语句的支持。这表明开发一门语言并随着新想法的发现逐步增加额外的功能是多么容易。

我们的最终结果将允许我们在Kaleidoscope中编写这种if-then-else语句

def fib(x)
  if x < 3 then
    1
  else
    fib(x-1)+fib(x-2);

由于Kaleidoscope中的所有内容都是一个表达式,所以if-then-else表达式也需要返回一个值。如果它对表达式的评估为假,则返回0,否则,表达式的评估为真。另外,如果条件为真,第一个子表达式被评估,然后返回,否则,如果条件为假,第二个子表达式被评估,然后返回。

词典扩展

在本节中,我们将在词典中增加对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;

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节点由指向各种子表达式的指针组成。

解析器的扩展

解析器的输入是来自词典的一串标记,它验证这些标记是否可以由Kaleidoscope的语法使用AST生成。解析器还将负责报告在源代码中发现的任何语法错误,在这种情况下是关于if-then-else语句。在格式良好的程序中,解析器会构建一个解析树,并将其作为输出返回,然后传递给下面的编译阶段。

在我们的例子中,解析逻辑如下,首先我们定义一个新的解析函数,如下所示:

/// 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));
}

ParseIfExpr解析给它的输入,当在代码中发现错误时,它也会报告错误。
下一步是将上述函数作为一个主要的表达式来挂钩:

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

LLVM IR扩展

在这个阶段,我们现在准备生成与if-then-else语句相关的中间代码。首先,我们考虑以下问题:

extern foo();
extern bar();
def baz(x) if x then foo() else bar();

我们用上述内容来激励我们要产生的IR。在没有任何形式的优化的情况下,我们有如下的结果:

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
}

上述内容的可视化效果如下所示:

control-flow

我们可以用LLVM的 opt工具获得这个结果。首先,我们将代码编译成LLVM IR,文件扩展名为*.ll*。然后执行命令;llvm-as < t.ll | opt -passes=view-cfg。结果就是上面的图片。这是LLVM众多图形可视化功能中的一个。

入口块评估了条件表达式x,然后使用fcmp one指令将结果与0.0进行比较。结果决定了代码是否跳转到thenelse块,这些块包含了真/假情况的表达式。
if/then
块完成后,它们会分支回到
ifcont块,执行if/then/else
语句之后的代码。

剩下的部分是返回给调用者。代码如何知道要返回哪个表达式,取决于一个重要的SSA操作--phi操作

phi操作的执行需要记住区块的原点。它采用的是与输入控制块相对应的值。如果控制来自then块,它将获得calltmp值,否则如果来自else块,它将被分配calltmp1值。

代码生成扩展

最后,将if-then-else条件语句纳入我们语言的最后一步在于代码生成。我们为IfExprAST实现了一个codegen方法,为条件发射一个表达式。然后我们将发出的值与零进行比较,得到一个真值,作为一个1位的boolen值。我们的代码如下:

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

接下来,我们创建与if/then/else语句相关的基本块:

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

首先,我们得到当前正在构建的Function对象,然后创建三个块,发射出在它们之间进行选择的条件分支。

创建块并不影响IRBuilder,因此它仍然被插入到条件进入的块中。

在条件分支插入后,我们移动构建器,开始插入到then块中。这就把插入点移到了块的末端。

Builder.SetInsertPoint(ThenBB); // emit then value

Value *ThenV = Then->codegen();

if (!ThenV)
    return nullptr;

Builder.CreateBr(MergeBB);

// codegen for 'Then' can change the current block, and update ThenBB for the PHI.
ThenBB = Builder.GetInsertBlock();

当插入点被设定后,我们从AST中编入then表达式。我们使用递归来实现这一目的。为了完成then块,我们创建一个无条件的分支到merge块。在LLVM中,所有的控制流,包括fall through,都必须在LLVM的IR中明确。

else块的代码生成与then块类似:

TheFunction->getBasicBlockList().push_back(ElseBB); // emit else block
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块添加到函数中。

最后,我们完成合并代码:

    TheFunction->getBasicBlockList().push_back(MergeBB); // emit merge block
    
    Builder.SetInsertPoint(MergeBB);
    
    PHINode *PN = Builder.CreatePHI(Type::getDoubleTy(TheContext), 2, "iftmp");

    PN->addIncoming(ThenV, ThenBB);
    PN->addIncoming(ElseV, ElseBB);
    
    return PN;
}

第一行将合并块添加到Function对象中;
第二行改变插入点,使新创建的代码进入合并块。
第三行,我们创建PHI节点并为PHI

设置块/值对。

最后,CodeGen函数通过if/then/else表达式返回PHI节点的计算值,这个值被送入创建返回指令的顶级函数代码中。

现在我们可以在这种编程语言中执行条件代码了。

总结

控制流语句是用来改变程序执行流程的语句。我们通过在词典、解析器、AST以及最后的LLVM代码生成器中支持控制流,实现了对**的支持。

参考文献

  1. 代码生成器中的控制流
  2. LLVM控制流:for循环。