LLVM官方教程Chap 3
Published on 2022-01-22 22:35 in 暂未分类 with wztuuu

LLVM官方教程Chap 3

    note:

    1. 需要LLVM3.7及以上版本
    2. 你会发现这些教程是自底向上描述的,可能初读有些费解,唯一的方法就是多读几遍。

    设置

    首先进行一些设置,主要是为所有抽象语法树中添加codegen()函数

    /// ExprAST - 所有表达式结点由此继承
    /// Base class for all expression nodes.
    class ExprAST {
    public:
      virtual ~ExprAST() {}
      virtual Value *codegen() = 0;
    };
    
    /// NumberExprAST - 数值型表达式,比如“1.0”
    /// Expression class for numeric literals like "1.0".
    class NumberExprAST : public ExprAST {
      double Val;
    
    public:
      NumberExprAST(double Val) : Val(Val) {}
      virtual Value *codegen();
    };
    ...
    

    codegen()方法会生成所有语法树节点的中间表示,返回一个LLVM的Value对象。

    Value对象是LLVM中用来表达SSA值的类型

    第二步是添加一个LogErrorV函数用于代码生成过程中的错误信息提示。

    Value *LogErrorV(const char *Str) {
      LogError(Str);
      return nullptr;
    }
    

    还需要一些LLVM中的数据结构

    • TheContext中包含很多LLVM的核心数据结构,比如类型表和常量值表
    • Builder对象用来追踪在何处生成代码
    • TheModule包含了函数和全局变量,他是LLVM IR用来包含代码的顶级结构,拥有所生成的IR的全部内存
    • NamedValues用来追踪在当前作用域中定义了哪些变量,目前来说,它只对函数参数起作用,在生成函数体的代码时,NamedValues中保存的就是所有参数
    static std::unique_ptr<LLVMContext> TheContext;
    static std::unique_ptr<IRBuilder<>> Builder(*TheContext);
    static std::unique_ptr<Module> TheModule;
    static std::map<std::string, Value *> NamedValues;
    

    3.3 表达式的代码生成

    1. 数值

      在LLVM中,在 LLVM IR 中,数值常量用 ConstantFP 类表示,该类在内部APFloat 中保存数值(APFloat具有保存任意精度的浮点常量的能力)。这段代码基本上只是创建并返回一个 ConstantFP。注意,在 LLVM IR 中,常量都是唯一的,并且是共享的。因此,API使用了foo::get(...)习惯用法,而不是new foo()。或者foo::Create()

      Value *NumberExprAST::codegen() {
        return ConstantFP::get(TheContext, APFloat(Val));
      }
      
    2. 变量

      如上文所说,在我们目前的设计中变量的代码生成只会在函数的形参中出现,而此时变量都保存在NamedValues这个map中,因此我们只需要根据对应的变量名找到其Value对象即可,在下文中会提及NamedValues表的处理,及这些变量是如何添加到表中的。

      Value *VariableExprAST::codegen() {
        // Look this variable up in the function.
        Value *V = NamedValues[this->Name];
        if (!V)
          LogErrorV("Unknown variable name");
        return V;
      }
      

    3. 二元表达式

      根据前面的Parse过程,二元表达式包含了LHS,RHS和操作符Op。LHS和RHS的类型有自己的codegen()方法,直接调用就可以,重点即是在处理Op的过程。

      Value *BinaryExprAST::codegen() {
        Value *L = LHS->codegen();
        Value *R = RHS->codegen();
        if (!L || !R)
          return nullptr;
      
        switch (Op) {
        case '+':
          return Builder.CreateFAdd(L, R, "addtmp");
        case '-':
          return Builder.CreateFSub(L, R, "subtmp");
        case '*':
          return Builder.CreateFMul(L, R, "multmp");
        case '<':
          L = Builder.CreateFCmpULT(L, R, "cmptmp");
          // Convert bool 0/1 to double 0.0 or 1.0
          return Builder.CreateUIToFP(L, Type::getDoubleTy(TheContext),
                                      "booltmp");
        default:
          return LogErrorV("invalid binary operator");
        }
      }
      

      在上面的示例中,LLVM builder类开始工作。IRBuilder知道在哪里插入新创建的指令,我们所要做的就是指定要创建什么指令(例如使用CreateFAdd) ,要使用哪个操作数(比如代码中的L和R),并有选择地为生成的指令提供一个名称。

      注意刚刚所说的名称,即代码中的addtmp,subtmp等,只是一个提示,如果出现多个同名,LLVM自动为每个名称添加一个递增的后缀

      LLVM指令有严格的规则约束:例如,添加指令的左操作符和右操作符必须具有相同的类型,并且添加的结果类型必须与操作数类型匹配。因为万花筒中的所有值都是双精度的,这使得add、sub和mul的代码非常简单。

      另一方面,LLVM指定fcmp指令总是返回一个'i1'值(一个1位整数)。问题在于Kaleidoscope希望值是0.0或1.0。为了得到这些语义,我们将fcmp指令与uitofp指令结合起来。该指令将输入的整数作为无符号值处理,从而将其转换为浮点值。相反,如果我们使用sitofp指令,Kaleidoscope的'<'操作符将返回0.0和-1.0,这取决于输入值。

    4. 函数调用

      使用 LLVM 生成函数调用的代码非常简单。上面的代码首先在 LLVM 的Module的符号表中查找函数名。回想一下,LLVM Module是容纳我们要JIT的函数的容器。通过赋予每个函数与用户指定的函数相同的名称,我们可以使用 LLVM 符号表为我们解析函数名。

      Value *CallExprAST::codegen() {
        // Look up the name in the global module table.
      	// 在Mudole中查找函数名
        Function *CalleeF = TheModule->getFunction(this->Callee);
        if (!CalleeF)
          return LogErrorV("Unknown function referenced");
      
        // If argument mismatch error.
      	// 如果参数对应不上
        if (CalleeF->arg_size() != this->Args.size())
          return LogErrorV("Incorrect # arguments passed");
      
        std::vector<Value *> ArgsV;
        for (unsigned i = 0, e = this->Args.size(); i != e; ++i) {
      		// 这里的Args[i]的类型基类是ExprAST
          ArgsV.push_back(this->Args[i]->codegen());
          if (!ArgsV.back())
            return nullptr;
        }
      
        return Builder.CreateCall(CalleeF, ArgsV, "calltmp");
      }
      
    5. END

      到目前为止,我们在《万花筒》中对四个基本表达式的处理就到此结束了。请随意进入并添加更多内容。例如,通过浏览LLVM语言参考,你会发现其他几个有趣的指令,它们非常容易插入我们的基本框架。

    函数代码的生成

    原型和函数的代码生成必须处理大量的细节,这使得它们的代码没有上文那些表达式的代码生成那么优美。首先,让我们讨论一下原型ProtoType的代码生成:它们既用于函数体,也用于外部函数声明。代码的开头是:

    Function *PrototypeAST::codegen() {
      // Make the function type:  double(double,double) etc.
    
    	// hint: 这里的this->Args的类型是vector<string>
      std::vector<Type*> Doubles(this->Args.size(),
                                 Type::getDoubleTy(TheContext));
      FunctionType *FT =
        FunctionType::get(Type::getDoubleTy(TheContext), Doubles, false);
    
      Function *F =
        Function::Create(FT, Function::ExternalLinkage, Name, TheModule.get());
    

    这段代码在几行代码中包含了很多功能。首先要注意,这个函数返回的是“Function*”而不是“Value*”。因为“Prototype”真正谈论的是函数的外部接口(不是由表达式计算的值),所以它返回编码时所对应的LLVM函数是有意义的。

    上面这段代码一共有3行:

    1. 第一行:

      因为Kaleidoscope中的所有函数参数都是double类型的,所以第一行创建了一个大小为N的LLVM double类型的向量,这表示所有参数的类型。

    2. 第二行

      然后,它使用Functiontype::get方法创建一个函数类型,该函数类型以N个double类型作为参数类型,一个double作为返回值类型。false参数表示该函数的参数不可变长。

      注意,LLVM 中的类型是uniqued的,就像常量一样,所以不需要new一个类型,而是get

    3. 第三行

      上面的最后一行实际上创建了与Prototype对应的一个IR Function。

      IR Function指示了要使用的类型(double→返回值 (N个Double→参数))、链接和函数名称,以及要插入到哪个模块。

      Function::ExternalLinkage”意味着该函数可以在当前模块之外定义,并且/或者可以由模块之外的函数调用。

      传入的 Name 是用户指定的名称: 因为指定了“TheModule”,所以这个名称注册在“ TheModule”的符号表中。


    // Set names for all arguments.
    unsigned Idx = 0;
    for (auto &Arg : F->args())
      Arg.setName(this->Args[Idx++]);
    
    return F;
    

    最后,我们根据Prototype中给出的名称设置每个函数参数的名称。这一步并不是严格必要的,但是保持名称的一致性可以使IR更具可读性,并且允许后续代码直接引用名称的参数,而不必在Prototype AST中查找它们。

    现在,我们有了一个没有主体的函数原型。这就是LLVM IR表示函数声明的方式。

    对于Kaleidoscope中的extern语句而言,到这一步就可以完全处理掉一个extern语句了。但是,对于自定义的有主体的函数,我们需还要codegen并附加一个函数体。

    Function *FunctionAST::codegen() {
        // First, check for an existing function from a previous 'extern' declaration.
    	// 前文中我们通过:Function::Create(..., Name, TheModule.get())将Name添加到Module中了
      Function *TheFunction = TheModule->getFunction(this->Proto->getName());
    
      if (!TheFunction)
        TheFunction = this->Proto->codegen();
    
      if (!TheFunction)
        return nullptr;
    
      if (!TheFunction->empty())
        return (Function*)LogErrorV("Function cannot be redefined.");
    

    对于函数定义,我们首先在模块的符号表中查找这个函数的现有版本,以防已经使用'extern'语句创建了一个函数。如果Module::getFunction返回null,那么之前的版本不存在,所以我们将从原型中codegen()。在这两种情况下,我们都希望在开始之前保证函数体为空(即还没有函数体)。

    接下来将创建函数体

    // Create a new basic block to start insertion into.
    BasicBlock *BB = BasicBlock::Create(TheContext, "entry", TheFunction);
    Builder.SetInsertPoint(BB);
    
    // Record the function arguments in the NamedValues map.
    NamedValues.clear();
    for (auto &Arg : TheFunction->args())
      NamedValues[Arg.getName()] = &Arg;
    

    现在我们到了Builder的部分。第一行创建一个新的basic block(名为“entry”,在接之后的运行过程中我们会看到这个entry的位置),并将其插入到TheFunction中。

    然后,第二行告诉builder,新的指令应该插入到新的基本块的末尾。

    LLVM 中的基本块是定义控制流程图的函数的一个重要部分。因为我们没有任何控制流,所以我们的函数此时只包含一个块。我们将在第5章解决这个问题:)。

    接下来,我们将函数参数添加到 NamedValues 映射(在首次清除它之后) ,以便可以访问 VariableExprAST 节点。

    // 这里的Body是一个expression,递归调用它自身的codegen()方法
    if (Value *RetVal = Body->codegen()) {
      // Finish off the function.
      Builder.CreateRet(RetVal);
    
      // Validate the generated code, checking for consistency.
      verifyFunction(*TheFunction);
    
      return TheFunction;
    }
    

    一旦设置好插入点并填充了 NamedValues 映射,我们就为函数的根表达式调用 codegen()方法。如果没有错误发生,则发出代码以计算条目块中的表达式,并返回计算得到的值。假设没有错误,然后我们创建一个 LLVM ret instruction,完成这个函数。构建函数后,我们调用由 LLVM 提供的 verifyFunction。这个函数对生成的代码进行各种一致性检查,以确定我们的编译器是否一切正常。使用这一点很重要: 它可以捕捉到很多 bug。一旦完成并验证了函数,我们就返回它。

    这里剩下的唯一部分是处理错误情况。为了简单起见,我们仅仅通过删除 eraseFromParent 方法生成的函数来处理这个问题。这允许用户重新定义他们之前输入的错误的函数: 如果我们没有删除它,它将与一个主体一起存在于符号表中,防止将来重新定义。

    // Error reading body, remove function.
      TheFunction->eraseFromParent();
      return nullptr;
    }
    
    posted @   wztuuu  阅读(366)  评论(0编辑  收藏  举报
    相关博文:
    阅读排行:
    · 震惊!C++程序真的从main开始吗?99%的程序员都答错了
    · 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
    · 单元测试从入门到精通
    · 上周热点回顾(3.3-3.9)
    · winform 绘制太阳,地球,月球 运作规律
    点击右上角即可分享
    微信分享提示