编程语言中的突变涉及到改变一个对象的能力。在这篇文章中,我们进一步扩展了我们的语言,我们给它增加了定义新变量和变异的能力。
目录:
- 简介
- 调整现有变量的突变
- 新的赋值运算符
- 用户定义的局部变量
- 总结
- 参考资料
先决条件
引言。
在这篇文章中,我们将讨论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增加了定义新变量和变异变量的能力。我们使用*'='*运算符来变异变量。