实现第一个语言前端LLVM教程(七)扩展语言:可变变量

7.1. 简介

欢迎来到"用LLVM实现语言"教程的第7章。在第1章到第6章中,我们构建了一个非常受人尊敬的函数式编程语言,尽管它很简单。在我们的旅程中,我们学习了一些解析技术,如何构建和表示AST,如何构建LLVM IR,以及如何优化结果代码以及JIT编译它。

虽然Kaleidoscope作为一种函数式语言很有趣,但它的函数性使得为它生成LLVM IR"太容易"了。特别是,函数式语言使得以SSA形式直接构建LLVM IR非常容易。由于LLVM要求输入代码是SSA形式的,这是一个非常好的属性,对于新手来说,如何为带有可变变量的命令式语言生成代码通常是不清楚的。

本章的简短总结是,您的前端不需要构建SSA表单:LLVM为此提供了高度调优和经过良好测试的支持,尽管它的工作方式对某些人来说有点出乎意料

7.2. 为什么这是一个难题?

要理解为什么可变变量会导致SSA构造的复杂性,请考虑这个极其简单的C示例:

ini 复制代码
int G, H;
int test(_Bool Condition) {
  int X;
  if (Condition)
    X = G;
  else
    X = H;
  return X;
}

在本例中,我们有变量"X",其值取决于程序中执行的路径。因为在返回指令之前X有两个不同的可能值,所以插入一个PHI节点来合并这两个值。我们在这个例子中想要的LLVM IR是这样的:

perl 复制代码
@G = weak global i32 0   ; type of @G is i32*
@H = weak global i32 0   ; type of @H is i32*

define i32 @test(i1 %Condition) {
entry:
  br i1 %Condition, label %cond_true, label %cond_false

cond_true:
  %X.0 = load i32, i32* @G
  br label %cond_next

cond_false:
  %X.1 = load i32, i32* @H
  br label %cond_next

cond_next:
  %X.2 = phi i32 [ %X.1, %cond_false ], [ %X.0, %cond_true ]
  ret i32 %X.2
}

在这个例子中,来自G和H全局变量的加载在LLVM IR中是显式的,它们存在于if语句的then/else分支中(cond_true/cond_false)。为了合并传入的值,cond_next块中的X.2 phi节点根据控制流的来源选择正确的值:如果控制流来自cond_false块,X.2获得X.1的值。或者,如果控制流来自cond_true,它将获得X.0的值。本章的目的不是解释SSA表单的细节。欲了解更多信息,请参阅网上众多网站之一

本文的问题是"在降低对可变变量的赋值时,谁放置phi节点?"这里的问题是LLVM要求它的IR是SSA形式:它没有非"SSA"模式。然而,SSA的构建需要重要的算法和数据结构,因此每个前端都必须重新生成这种逻辑是不方便和浪费的。

7.3. LLVM中的内存

这里的"技巧"是,虽然LLVM确实要求所有寄存器值以SSA形式存在,但它不要求(或允许)内存对象以SSA形式存在。在上面的例子中,请注意,来自G和H的加载是对G和H的直接访问:它们没有重命名或版本控制。这与其他一些编译器系统不同,它们会尝试对内存对象进行版本化。在LLVM中,不是将内存的数据流分析编码到LLVM IR中,而是使用按需计算的Analysis Passes来处理。

考虑到这一点,高级的想法是我们想要为函数中的每个可变对象创建一个堆栈变量(它存在于内存中,因为它在堆栈上)。为了利用这个技巧,我们需要讨论LLVM如何表示堆栈变量。

在LLVM中,所有的内存访问都是显式的加载/存储指令,并且它被精心设计为不具有(或不需要)"地址"操作符。注意@G/@H全局变量的类型实际上是"i32*",尽管变量被定义为"i32"。这意味着@G在全局数据区为i32定义了空间,但它的名称实际上指的是该空间的地址。堆栈变量的工作方式相同,只不过不是用全局变量定义声明,而是用LLVM alloca instruction:指令声明:

ini 复制代码
define i32 @example() {
entry:
  %X = alloca i32           ; type of %X is i32*.
  ...
  %tmp = load i32, i32* %X  ; load the stack value %X from the stack.
  %tmp2 = add i32 %tmp, 1   ; increment it
  store i32 %tmp2, i32* %X  ; store it back
  ...

这段代码展示了如何在LLVM IR中声明和操作堆栈变量的示例。用alloca指令分配的堆栈内存是完全通用的:你可以把堆栈槽的地址传递给函数,你可以把它存储在其他变量中,等等。在上面的示例中,我们可以重写示例以使用alloca技术来避免使用PHI节点:

perl 复制代码
@G = weak global i32 0   ; type of @G is i32*
@H = weak global i32 0   ; type of @H is i32*

define i32 @test(i1 %Condition) {
entry:
  %X = alloca i32           ; type of %X is i32*.
  br i1 %Condition, label %cond_true, label %cond_false

cond_true:
  %X.0 = load i32, i32* @G
  store i32 %X.0, i32* %X   ; Update X
  br label %cond_next

cond_false:
  %X.1 = load i32, i32* @H
  store i32 %X.1, i32* %X   ; Update X
  br label %cond_next

cond_next:
  %X.2 = load i32, i32* %X  ; Read X
  ret i32 %X.2
}

有了这个,我们发现了一种处理任意可变变量而不需要创建Phi节点的方法:

  1. 每个可变变量都成为一个堆栈分配。
  2. 对变量的每次读取都变成了对堆栈的一次加载。
  3. 变量的每次更新都成为堆栈的存储。
  4. 获取变量的地址只是直接使用堆栈地址。

虽然这个解决方案解决了我们眼前的问题,但它引入了另一个问题:我们现在显然为非常简单和常见的操作引入了大量堆栈流量,这是一个主要的性能问题。对我们来说幸运的是,LLVM优化器有一个名为"mem2reg"的高度调优优化pass来处理这种情况,将这样的分配提升到SSA寄存器中,并适当地插入Phi节点。例如,如果您通过pass运行此示例,您将得到:

perl 复制代码
$ llvm-as < example.ll | opt -passes=mem2reg | llvm-dis
@G = weak global i32 0
@H = weak global i32 0

define i32 @test(i1 %Condition) {
entry:
  br i1 %Condition, label %cond_true, label %cond_false

cond_true:
  %X.0 = load i32, i32* @G
  br label %cond_next

cond_false:
  %X.1 = load i32, i32* @H
  br label %cond_next

cond_next:
  %X.01 = phi i32 [ %X.1, %cond_false ], [ %X.0, %cond_true ]
  ret i32 %X.01
}

mem2reg pass实现了用于构造SSA形式的标准Iterated dominance frontier算法,并具有许多加速退化情况的优化。mem2reg优化传递是处理可变变量的答案,我们强烈建议您依赖它。请注意,mem2reg仅在某些情况下对变量起作用:

  1. Mem2reg是分配驱动的:它寻找分配,如果它能处理它们,它就提升它们。它不适用于全局变量或堆分配。
  2. Mem2reg只在函数的入口块中查找alloca指令。在入口块中保证了分配只执行一次,这使得分析更简单。
  3. Mem2reg仅提升用于直接加载和存储的分配。如果堆栈对象的地址被传递给一个函数,或者涉及到任何奇怪的指针算术,则分配将不会被提升。
  4. Mem2reg仅适用于第一类值的分配(如指针、标量和向量),并且仅当分配的数组大小为1(或在.ll文件中缺失)时才有效。Mem2reg不能将结构体或数组提升到寄存器。请注意,"sroa"传递更强大,在许多情况下可以提升结构、"union"和数组。

对于大多数命令式语言来说,所有这些属性都很容易满足,我们将在下面用(Kaleidoscope)进行说明。您可能会问的最后一个问题是:我应该为我的前端使用这些无意义的东西吗?如果我直接进行SSA构建,避免使用mem2reg优化pass,不是更好吗?简而言之,我们强烈建议您使用这种技术来构建SSA表单,除非有非常好的理由不这样做。使用这种技术是:

  • 经过验证和良好测试:clang对局部可变变量使用这种技术。因此,LLVM最常见的客户端都使用它来处理大量的变量。您可以确保快速发现并尽早修复错误。•
  • 非常快:mem2reg有许多特殊的情况,使它在普通情况下非常快,并且完全通用。例如,它有只在单个块中使用的变量的快速路径,只有一个赋值点的变量,良好的启发式算法以避免插入不必要的phi节点,等等。
  • 生成调试信息所需:LLVM中的调试信息依赖于公开变量的地址,以便调试信息可以附加到它。这种技术非常自然地与这种调试信息风格相吻合。

如果没有别的,这使您的前端更容易启动和运行,并且非常容易实现。现在让我们用可变变量扩展Kaleidoscope!

7.4. 可变变量

现在我们知道了我们想要解决的问题,让我们看看在Kaleidoscope语言的背景下它是什么样子的。我们将添加两个功能:

  1. 使用' = '操作符改变变量的能力。
  2. 定义新变量的能力。

虽然第一项是真正的问题所在,但我们只有传入参数和归纳变量的变量,并且重新定义这些变量只能到此为止:)。此外,定义新变量的能力非常有用,无论您是否会对它们进行改变。这里有一个鼓舞人心的例子,告诉我们如何使用这些:

ini 复制代码
# Define ':' for sequencing: as a low-precedence operator that ignores operands
# and just returns the RHS.
def binary : 1 (x y) y;

# Recursive fib, we could do this before.
def fib(x)
  if (x < 3) then
    1
  else
    fib(x-1)+fib(x-2);

# Iterative fib.
def fibi(x)
  var a = 1, b = 1, c in
  (for i = 3, i < x in
     c = a + b :
     a = b :
     b = c) :
  b;

# Call it.
fibi(10);

为了改变变量,我们必须使用"alloca技巧"来改变现有的变量。完成之后,我们将添加新的操作符,然后扩展Kaleidoscope以支持新的变量定义。

7.5. 调整现有可变变量

Kaleidoscope中的符号表在代码生成时由'NamedValues'映射管理。这个映射当前跟踪LLVM"Value*",它保存了指定变量的双精度值。为了支持可变,我们需要稍微改变一下,以便NamedValues保存相关变量的内存位置。注意,这种改变是重构:它改变了代码的结构,但(本身)没有改变编译器的行为。所有这些变化都被隔离在Kaleidoscope代码生成器中。

在Kaleidoscope的开发过程中,它只支持两种变量:函数的传入参数和for循环的推导变量。为了保持一致性,除了其他用户定义的变量之外,我们还允许对这些变量进行改变。这意味着它们都需要内存位置。

为了开始我们对Kaleidoscope的转换,我们将更改NamedValues映射,使其映射到AllocaInst而不是Value。这样做后,c++编译器会告诉我们需要更新的代码部分:

c 复制代码
static std::map<std::string, AllocaInst*> NamedValues;

此外,由于我们需要创建这些分配,我们将使用一个辅助函数来确保分配是在函数的入口块中创建的:

scss 复制代码
/// CreateEntryBlockAlloca - Create an alloca instruction in the entry block of
/// the function.  This is used for mutable variables etc.
static AllocaInst *CreateEntryBlockAlloca(Function *TheFunction,
                                          StringRef VarName) {
  IRBuilder<> TmpB(&TheFunction->getEntryBlock(),
                 TheFunction->getEntryBlock().begin());
  return TmpB.CreateAlloca(Type::getDoubleTy(*TheContext), nullptr,
                           VarName);
}

这段看起来很有趣的代码创建了一个IRBuilder对象,它指向入口块的第一条指令(.begin())。然后,它使用期望的名称创建一个分配并返回它。因为Kaleidoscope中的所有值都是double类型,所以不需要传入要使用的类型。

在此基础上,我们想要进行的第一个功能更改属于变量引用。在我们的新方案中,变量存在于堆栈中,因此代码生成对它们的引用实际上需要从堆栈槽中产生加载:

scss 复制代码
Value *VariableExprAST::codegen() {
  // Look this variable up in the function.
  AllocaInst *A = NamedValues[Name];
  if (!A)
    return LogErrorV("Unknown variable name");

  // Load the value.
  return Builder->CreateLoad(A->getAllocatedType(), A, Name.c_str());
}

如您所见,这非常简单。现在我们需要更新定义变量以设置分配的内容。我们将从ForExprAST::codegen()开始(请参阅完整的代码清单以获取未删节的代码):

scss 复制代码
Function *TheFunction = Builder->GetInsertBlock()->getParent();

// Create an alloca for the variable in the entry block.
AllocaInst *Alloca = CreateEntryBlockAlloca(TheFunction, VarName);

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

// Store the value into the alloca.
Builder->CreateStore(StartVal, Alloca);
...

// Compute the end condition.
Value *EndCond = End->codegen();
if (!EndCond)
  return nullptr;

// Reload, increment, and restore the alloca.  This handles the case where
// the body of the loop mutates the variable.
Value *CurVar = Builder->CreateLoad(Alloca->getAllocatedType(), Alloca,
                                    VarName.c_str());
Value *NextVar = Builder->CreateFAdd(CurVar, StepVal, "nextvar");
Builder->CreateStore(NextVar, Alloca);
...

这段代码实际上与允许可变变量之前的代码相同。最大的不同是,我们不再需要构造PHI节点,我们使用load/store来根据需要访问变量。

为了支持可变参数变量,我们还需要为它们做分配。这样做的代码也很简单:

scss 复制代码
Function *FunctionAST::codegen() {
  ...
  Builder->SetInsertPoint(BB);
  // Record the function arguments in the NamedValues map.
  NamedValues.clear();
  for (auto &Arg : TheFunction->args()) {
    // Create an alloca for this variable.
    AllocaInst *Alloca = CreateEntryBlockAlloca(TheFunction, Arg.getName());

    // Store the initial value into the alloca.
    Builder->CreateStore(&Arg, Alloca);

    // Add arguments to variable symbol table.
    NamedValues[std::string(Arg.getName())] = Alloca;
  }
  if (Value *RetVal = Body->codegen()) {
    ...

对于每个实参,我们创建一个alloca,将函数的输入值存储到该alloca中,并将该alloca注册为实参的内存位置。此方法在为函数设置入口块之后立即由FunctionAST::codegen()调用。

最后缺失的部分是添加mem2reg pass,它允许我们再次获得良好的代码:

scss 复制代码
// Promote allocas to registers.
TheFPM->addPass(PromotePass());
// Do simple "peephole" optimizations and bit-twiddling optzns.
TheFPM->addPass(InstCombinePass());
// Reassociate expressions.
TheFPM->addPass(ReassociatePass());
...

看看运行mem2reg优化之前和之后的代码是什么样子是很有趣的。例如,这是我们递归fib函数的前后代码。优化前:

perl 复制代码
define double @fib(double %x) {
entry:
  %x1 = alloca double
  store double %x, double* %x1
  %x2 = load double, double* %x1
  %cmptmp = fcmp ult double %x2, 3.000000e+00
  %booltmp = uitofp i1 %cmptmp to double
  %ifcond = fcmp one double %booltmp, 0.000000e+00
  br i1 %ifcond, label %then, label %else

then:       ; preds = %entry
  br label %ifcont

else:       ; preds = %entry
  %x3 = load double, double* %x1
  %subtmp = fsub double %x3, 1.000000e+00
  %calltmp = call double @fib(double %subtmp)
  %x4 = load double, double* %x1
  %subtmp5 = fsub double %x4, 2.000000e+00
  %calltmp6 = call double @fib(double %subtmp5)
  %addtmp = fadd double %calltmp, %calltmp6
  br label %ifcont

ifcont:     ; preds = %else, %then
  %iftmp = phi double [ 1.000000e+00, %then ], [ %addtmp, %else ]
  ret double %iftmp
}

这里只有一个变量(x,输入参数),但您仍然可以看到我们使用的极其简单的代码生成策略。在入口块中,创建了一个alloca,并将初始输入值存储在其中。对变量的每次引用都会从堆栈中重新加载一次。另外,请注意,我们没有修改if/then/else表达式,因此它仍然插入一个PHI节点。虽然我们可以为它创建一个分区,但实际上为它创建一个PHI节点更容易,所以我们仍然只创建PHI。

下面是mem2reg pass 运行后的代码:

perl 复制代码
define double @fib(double %x) {
entry:
  %cmptmp = fcmp ult double %x, 3.000000e+00
  %booltmp = uitofp i1 %cmptmp to double
  %ifcond = fcmp one double %booltmp, 0.000000e+00
  br i1 %ifcond, label %then, label %else

then:
  br label %ifcont

else:
  %subtmp = fsub double %x, 1.000000e+00
  %calltmp = call double @fib(double %subtmp)
  %subtmp5 = fsub double %x, 2.000000e+00
  %calltmp6 = call double @fib(double %subtmp5)
  %addtmp = fadd double %calltmp, %calltmp6
  br label %ifcont

ifcont:     ; preds = %else, %then
  %iftmp = phi double [ 1.000000e+00, %then ], [ %addtmp, %else ]
  ret double %iftmp
}

对于mem2reg来说,这是一个微不足道的例子,因为没有对变量进行重新定义。 在其余的优化器运行之后,我们得到:

perl 复制代码
define double @fib(double %x) {
entry:
  %cmptmp = fcmp ult double %x, 3.000000e+00
  %booltmp = uitofp i1 %cmptmp to double
  %ifcond = fcmp ueq double %booltmp, 0.000000e+00
  br i1 %ifcond, label %else, label %ifcont

else:
  %subtmp = fsub double %x, 1.000000e+00
  %calltmp = call double @fib(double %subtmp)
  %subtmp5 = fsub double %x, 2.000000e+00
  %calltmp6 = call double @fib(double %subtmp5)
  %addtmp = fadd double %calltmp, %calltmp6
  ret double %addtmp

ifcont:
  ret double 1.000000e+00
}

在这里,我们看到simplifycfg传递决定将返回指令克隆到'else'块的末尾。这使得它可以消除一些分支和PHI节点。

现在,所有符号表引用都更新为使用堆栈变量,我们将添加赋值操作符。

7.6. 新赋值操作符

在我们当前的框架中,添加一个新的赋值操作符非常简单。我们将像解析任何其他二元运算符一样解析它,但在内部处理它(而不是允许用户定义它)。第一步是设置优先级:

less 复制代码
int main() {
  // 1 是最低优先级
  BinopPrecedence['='] = 2;
  BinopPrecedence['<'] = 10;
  BinopPrecedence['+'] = 20;
  BinopPrecedence['-'] = 20;

既然解析器知道了二元运算符的优先级,它就负责所有解析和AST生成。我们只需要实现赋值操作符的代码。这看起来像:

scss 复制代码
Value *BinaryExprAST::codegen() {
  // Special case '=' because we don't want to emit the LHS as an expression.
  if (Op == '=') {
    // This assume we're building without RTTI because LLVM builds that way by
    // default. If you build LLVM with RTTI this can be changed to a
    // dynamic_cast for automatic error checking.
    VariableExprAST *LHSE = static_cast<VariableExprAST*>(LHS.get());
    if (!LHSE)
      return LogErrorV("destination of '=' must be a variable");

与其他二元操作符不同,赋值操作符不遵循"弹出LHS,弹出RHS,进行计算"的模型。因此,在处理其他二元操作符之前,将其作为特殊情况处理。另一件奇怪的事情是它要求LHS是一个变量。"(x+1) = expr"是无效的-只允许"x = expr"这样的内容。

scss 复制代码
 // 生成右边表达式的代码
  Value *Val = RHS->codegen();
  if (!Val)
    return nullptr;

  // Look up the name.
  Value *Variable = NamedValues[LHSE->getName()];
  if (!Variable)
    return LogErrorV("Unknown variable name");

  Builder->CreateStore(Val, Variable);
  return Val;
}

有了变量之后,对赋值进行编码就很简单了:我们弹出赋值的RHS,创建一个存储,并返回计算值。返回一个值允许链式赋值,如"X = (Y = Z)"。

现在有了赋值操作符,就可以改变循环变量和参数了。例如,我们现在可以像这样运行代码:

ini 复制代码
# 打印double类型的数据
extern printd(x);

# 为sequence:定义':'作为忽略操作数的低优先级操作符
# 仅仅只返回右表达式
def binary : 1 (x y) y;

def test(x)
  printd(x) :
  x = 4 :
  printd(x);

test(123);

当运行时,这个示例打印"123",然后打印"4",表明我们确实改变了值!好的,我们现在已经正式实现了我们的目标:在一般情况下,要使它工作需要构建SSA。然而,为了真正有用,我们希望能够定义我们自己的局部变量,让我们接下来添加它!

7.7. 用户定义的局部变量

添加var/in就像我们对Kaleidoscope做的任何其他扩展一样:我们扩展词法分析器、解析器、AST和代码生成器。添加新的"var/in"构造的第一步是扩展词法分析器。和以前一样,这是相当琐碎的,代码看起来像这样

ini 复制代码
enum Token {
  ...
  // var definition
  tok_var = -13
...
}
...
static int gettok() {
...
    if (IdentifierStr == "in")
      return tok_in;
    if (IdentifierStr == "binary")
      return tok_binary;
    if (IdentifierStr == "unary")
      return tok_unary;
    if (IdentifierStr == "var")
      return tok_var;
    return tok_identifier;
...

下一步是定义我们将要构造的AST节点。对于var/in,它看起来像这样:

c 复制代码
// 为var 定义表达式类
class VarExprAST : public ExprAST {
  std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> VarNames;
  std::unique_ptr<ExprAST> Body;

public:
  VarExprAST(std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> VarNames,
             std::unique_ptr<ExprAST> Body)
    : VarNames(std::move(VarNames)), Body(std::move(Body)) {}

  Value *codegen() override;
};

Var /in允许一次定义所有的名称列表,每个名称可以有一个可选的初始化值。因此,我们在VarNames向量中捕获这些信息。另外,var/in有一个主体,这个主体可以访问var/in定义的变量。

在此基础上,我们可以定义解析器片段。我们要做的第一件事是将其作为主表达式添加:

c 复制代码
/// primary
///   ::= identifierexpr
///   ::= numberexpr
///   ::= parenexpr
///   ::= ifexpr
///   ::= forexpr
///   ::= varexpr
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();
  case tok_var:
    return ParseVarExpr();
  }
}

接下来我们定义解析器表达式:

c 复制代码
/// varexpr ::= 'var' identifier ('=' expression)?
//                    (',' identifier ('=' expression)?)* 'in' expression
static std::unique_ptr<ExprAST> ParseVarExpr() {
  getNextToken();  // eat the var.

  std::vector<std::pair<std::string, std::unique_ptr<ExprAST>>> VarNames;

  // At least one variable name is required.
  if (CurTok != tok_identifier)
    return LogError("expected identifier after var");

这段代码的第一部分将标识符/expr对的列表解析为本地VarNames向量。

c 复制代码
while (true) {
  std::string Name = IdentifierStr;
  getNextToken();  // eat identifier.

  // Read the optional initializer.
  std::unique_ptr<ExprAST> Init;
  if (CurTok == '=') {
    getNextToken(); // eat the '='.

    Init = ParseExpression();
    if (!Init) return nullptr;
  }

  VarNames.push_back(std::make_pair(Name, std::move(Init)));

  // End of var list, exit loop.
  if (CurTok != ',') break;
  getNextToken(); // eat the ','.

  if (CurTok != tok_identifier)
    return LogError("expected identifier list after var");
}

一旦所有的变量都被解析完,我们接着解析主体并创建AST节点

scss 复制代码
  // At this point, we have to have 'in'.
  if (CurTok != tok_in)
    return LogError("expected 'in' keyword after 'var'");
  getNextToken();  // eat 'in'.

  auto Body = ParseExpression();
  if (!Body)
    return nullptr;

  return std::make_unique<VarExprAST>(std::move(VarNames),
                                       std::move(Body));
}

现在我们可以解析和表示代码了,我们需要为它提供LLVM IR的支持。这段代码开始于

ini 复制代码
Value *VarExprAST::codegen() {
  std::vector<AllocaInst *> OldBindings;

  Function *TheFunction = Builder->GetInsertBlock()->getParent();

  // Register all variables and emit their initializer.
  for (unsigned i = 0, e = VarNames.size(); i != e; ++i) {
    const std::string &VarName = VarNames[i].first;
    ExprAST *Init = VarNames[i].second.get();

基本上,它循环遍历所有变量,一次安装一个。对于我们放入符号表中的每个变量,我们记住我们在OldBindings中替换的前一个值。

scss 复制代码
 // Emit the initializer before adding the variable to scope, this prevents
  // the initializer from referencing the variable itself, and permits stuff
  // like this:
  //  var a = 1 in
  //    var a = a in ...   # refers to outer 'a'.
  Value *InitVal;
  if (Init) {
    InitVal = Init->codegen();
    if (!InitVal)
      return nullptr;
  } else { // If not specified, use 0.0.
    InitVal = ConstantFP::get(*TheContext, APFloat(0.0));
  }

  AllocaInst *Alloca = CreateEntryBlockAlloca(TheFunction, VarName);
  Builder->CreateStore(InitVal, Alloca);

  // Remember the old variable binding so that we can restore the binding when
  // we unrecurse.
  OldBindings.push_back(NamedValues[VarName]);

  // Remember this binding.
  NamedValues[VarName] = Alloca;
}

这里的注释比代码还多。其基本思想是,我们发出初始化式,创建alloca,然后更新符号表以指向它。一旦所有的变量都被安装到符号表中,我们计算var/in表达式的主体:

scss 复制代码
// Codegen the body, now that all vars are in scope.
Value *BodyVal = Body->codegen();
if (!BodyVal)
  return nullptr;

最后,在返回之前,恢复之前的变量绑定:

css 复制代码
  // Pop all our variables from scope.
  for (unsigned i = 0, e = VarNames.size(); i != e; ++i)
    NamedValues[VarNames[i].first] = OldBindings[i];

  // Return the body computation.
  return BodyVal;
}

所有这一切的最终结果是我们得到了适当的作用域变量定义,我们甚至允许对它们进行改变:)。

有了这个,我们完成了我们开始做的事情。我们在介绍部分的迭代fib示例可以很好地编译和运行。mem2reg pass将我们所有的堆栈变量优化到SSA寄存器中,在需要的地方插入PHI节点,并且我们的前端仍然很简单:没有"iterated dominance frontier"计算。

完整代码

相关推荐
Ciderw16 天前
LLVM编译器简介
c++·golang·编译·编译器·gcc·llvm·基础设施
天枢破军20 天前
【AI】零代码-A卡780M核显在Windows平台运行ollama跑端侧大模型
llvm·deepseek
witton1 个月前
macOS使用LLVM官方发布的tar.xz来安装Clang编译器
vscode·macos·cmake·clang·llvm·qtcreator·clang++
高铭杰2 个月前
Postgresql源码(141)JIT系列分析汇总
postgresql·jit·llvm
Lhuu(重开版3 个月前
2024硬件科技协会LLVM第二次考核题解
算法·ast·llvm
Eloudy3 个月前
C CPP 中注释的正则表达式
正则表达式·llvm
CYRUS_STUDIO3 个月前
使用 opt 优化 LLVM IR,定制 clang 实现函数名加密
c++·性能优化·llvm
CYRUS STUDIO3 个月前
编译 LLVM 源码,使用 Clion 调试 clang
c语言·c++·visual studio·clang·ndk·llvm·clion
芦半山4 个月前
解读HWASan日志
android·linux·llvm