Kaleidoscope编程语言中的可变变量

245 阅读5分钟

编程语言中的突变涉及到改变一个对象的能力。在这篇文章中,我们进一步扩展了我们的语言,我们给它增加了定义新变量和变异的能力。

目录:

  1. 简介
  2. 调整现有变量的突变
  3. 新的赋值运算符
  4. 用户定义的局部变量
  5. 总结
  6. 参考资料

先决条件

LLVM内存。

引言。

在这篇文章中,我们将讨论Kaleidoscope编程语言中的可变变量。我们将增加两个功能,第一个是使用*'='*运算符突变变量的能力,第二个是定义新变量的能力。

一个例子:

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

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

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

# Call it.
fib(10);

对于我们来说,要突变变量,我们需要改变我们现有的变量,并使用alloca技巧,然后添加我们的新操作符,扩展Kaleidoscope以支持新变量的定义。

为突变调整现有变量

在以前的文章中,我们创建了一个NamedValues映射对象,它代表了Kaleidoscope的符号表,它在代码生成时被管理。
为了增加对突变的支持,我们需要改变它,使NamedValues持有变量的内存位置。
以上改变了代码结构,但没有改变编译器行为,这是一个代码重构。
在这个阶段,Kaleidoscope只支持两方面的变量,第一是函数的传入参数,第二是for

循环的归纳变量。为了保持一致性,我们允许这些变量和用户定义的变量的突变,这意味着它们都需要内存位置。

为了改造Kaleidoscope,我们改变了NamedValues,使其映射到AllocaInst而不是Value*。编译器会告诉我们需要更新的部分代码:

static std::map<std::string, AllocaInst*> NamedValues;

我们需要创建分配器,为此使用一个辅助函数,确保分配器在函数的入口块中被创建:

/// CreateEntryBlockAlloca - Create an alloca instruction in the entry block of
/// the function.  This is used for mutable variables etc.
static AllocaInst *CreateEntryBlockAlloca(Function *TheFunction,
                                          const std::string &VarName) {
  IRBuilder<> TmpB(&TheFunction->getEntryBlock(),
                 TheFunction->getEntryBlock().begin());
  return TmpB.CreateAlloca(Type::getDoubleTy(TheContext), 0,
                           VarName.c_str());
}

新的赋值运算符

添加一个新的赋值运算符很简单。我们像其他二进制运算符一样解析它,但在内部处理解析。
首先我们设置一个优先级,如下所示:

int main() {
  // Install standard binary operators.
  // 1 is lowest precedence.
  BinopPrecedence['='] = 2;
  BinopPrecedence['<'] = 10;
  BinopPrecedence['+'] = 20;
  BinopPrecedence['-'] = 20;

解析器知道二进制运算符的优先级,它将处理解析和AST的生成。我们只需要为赋值运算符实现codegen:

Value *BinaryExprAST::codegen() {
  // Special case '=' because we don't want to emit the LHS as an expression.
  if (Op == '=') {
    // Assignment requires the LHS to be an identifier.
    VariableExprAST *LHSE = dynamic_cast<VariableExprAST*>(LHS.get());
    if (!LHSE)
      return LogErrorV("destination of '=' must be a variable");

与其他二元运算符不同,我们的赋值运算符不遵循*"发出LHS,发出RHS,计算......",因此在处理其他二元运算符之前,它被作为一个特殊情况来处理。
另一个奇怪的地方是,它需要
LHS是一个变量。另外,"x + 1 = expr "是无效的,我们只有"x = expr "*
这样的东西:

  // Codegen the RHS.
  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)

由于我们有一个赋值运算符,我们可以改变循环变量和参数。也就是说,我们可以执行像下面这样的代码:

# Function to print a double.
extern printd(x);

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

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

test(123);

代码打印出123,然后是4,这表明我们实际上突变了值。我们还希望有能力定义我们自己的局部变量。这将在下一节讨论。

用户定义的局部变量

为了在Kaleidoscope中加入用户定义的局部变量,我们遵循同样的程序,首先,我们扩展词法器、解析器、AST,最后是代码生成器,以纳入用户定义的局部变量。

让我们来扩展词法器

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来说,它是如下的:

/// VarExprAST - Expression class for var/in
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
定义的变量。

现在,我们来扩展解析器

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

首先,我们把它作为一个主表达式加入。然后定义ParseVarExpr

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

然后我们解析主体,一旦所有的变量都被解析,我们就创建AST节点:

  // 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的排放,我们使用以下代码:

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中。

然后我们发出初始化器,创建分配器,并更新符号表以指向它:

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

一旦所有的变量被添加到符号表中,我们就评估var/in的主体:

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

最后我们在返回之前恢复之前的变量绑定。

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

结果是正确的范围内的变量定义是可变的

总结

mem2reg通道将我们的堆栈变量优化为SSA寄存器,在需要的地方插入PHI节点,而编译器的前端仍然是简单的。
在这篇文章中,我们为Kaleidoscope增加了定义新变量和变异变量的能力。我们使用*'='*运算符来变异变量。

参考资料

编译成目标代码