LLVM控制流程中的For循环

572 阅读6分钟

在编程中,我们使用循环来重复一连串的指令,直到满足一个指定的条件。在这篇文章中,我们进一步扩展了Kaleidoscope对循环的支持。

目录.

  1. 简介.
  2. for表达式.
  3. 词典扩展。
  4. AST的扩展。
  5. 解析器扩展。
  6. LLVM IR扩展。
  7. 代码生成扩展。
  8. 总结。
  9. 参考资料。

先决条件。

LLVM控制流:If-then-else。

介绍。

在先决条件文章中,我们学习了如何在Kaleidoscope编程语言中扩展对if-then-else语句的支持。在这篇文章中,我们进一步增加了循环的功能。

当我们想执行一个操作的所需次数,直到满足一个条件时,就会用到循环。我们可以有for循环while循环do while循环无限循环、上述循环的嵌套循环

我们也有循环控制语句,如breakcontinue,前者立即终止循环,而后者则跳到下一个迭代,跳过中间的任何代码。

for表达式。

例如,让我们使用for循环在Kaleidoscope中打印100颗星星***。

extern putchard(char);
def printstar(n)
  for i = 1, i < n, 1.0 in
    putchard(42);  # ascii 42 = '*'

# print 100 '*' characters
printstar(100);

在这种情况下,值42是代表星号的ASCII值。我们定义一个变量i,开始迭代。当条件--i < n为真时,我们将以一个步骤值来增加它。在这种情况下,我们的步长值是1.0。在其他情况下,如果没有指定步长,默认为1.0。当循环评估为真时,主体表达式被执行。

词典扩展。

词法分析器接受源代码并产生一系列的标记。在本节中,我们将通过创建新的标记和使用if else语句来识别标记,来增加对for循环的支持。

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

词法分析的输出是一个令牌流,然后被发送到解析器,解析器确保我们使用的是正确的语言语法,在本例中是Kaleidoscope。

AST的扩展。

抽象树是一种中间代码的形式,它们被用来以分层的形式表示源代码的结构。在我们的例子中,它可以归结为捕捉节点中的变量名和组成表达式。

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

解析器的扩展。

解析器的输入是一串来自词法分析的标记。解析器还负责报告源代码中有关本例中for语句的任何语法错误。在格式良好的程序中,解析器构建一个解析树,并将其作为输出返回,然后传递给下面的编译阶段。

在我们的例子中,唯一的问题是,我们如何处理一个可选的步骤值,为此,我们检查第二个逗号是否存在,如果不存在,它在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();
  }
}

LLVM IR扩展。

开发编译器前端的最后一步是生成一个IR(Intermediate Representation),之后可以使用后端将其转换为与机器相关的指令。参考第二节中的循环--在这里我们打印100个星号字符,我们有以下相应的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节点,表达式,和一个基本块。

代码生成扩展。

最后,要生成IR本身。首先我们输出循环值的起始表达式。

Value *ForExprAST::codegen() {
  // Emit the start code first, without 'variable' in scope.
  Value *StartVal = Start->codegen();
  if (!StartVal)
    return nullptr;

然后我们为循环体的开始设置LLVM的基本块。这里,循环主体是一个单一的块,主体也可以有多个块。
我们创建一个新的块,并按如下方式插入。

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

我们创建一个实际的块来启动循环,并为两个块之间的落差创建一个无条件的分支。

// 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.c_str());
Variable->addIncoming(StartVal, PreheaderBB);

由于for循环的前头已经设置好了,我们转而为for循环的主体发出代码。首先,我们移动插入点,为循环诱导变量创建PHI节点。

我们知道起始值的传入值,所以我们把它加到PHI节点上。Phi最终得到后边的第二个值。

// Within the loop, the variable is defined as 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循环给符号表引入了一个新的变量,这意味着符号表可以有函数参数或循环变量。
为此,它要求我们在编码循环主体之前,首先将循环变量作为其名称的当前值加入。
为了正确处理这个问题,我们要记住上面OldVal

中可能被影射的值。

一旦循环变量在符号表中被设置,代码就会递归地对主体进行编码。
这使得主体可以使用循环变量和任何对它的引用在符号表中找到它。

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

现在主体被发射出来了,我们通过添加步骤值或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");

现在我们评估循环的退出值,这里我们要确定是应该退出还是继续。

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

最后,对于清理工作。由于我们有NextVar的值,我们可以将传入的值添加到循环的PHI节点中。然后,我们将最后的循环变量从符号表中删除,这样它在for循环之后就不在范围内了。
最后,for循环的代码生成将总是返回0.0。

总结。

我们使用循环来重复一个指令序列,直到满足一个指定的条件,我们学习了如何在Kaleidoscope编程语言中添加for循环。
在下一篇文章中,我们将添加对用户定义的操作符的支持。