基于LLVM-自定义二元运算符的重载

下面就简单的介绍下,我们去构造 | (逻辑或运算符)作为样例,TOY语言当中的 | 运算符是这样使用的,其中前面的binary代表的是标识符,代表的是二元运算符的表示,然后后面就是| 运算符,再就是(LHS RHS) 根据这两个参数进行选择

def binary | 5 (LHS RHS)
if LHS then
1
else if RHS then
1
else
0;

这段代码的意思就是只要LHS或者是RHS任何一个不为0,那么就返回1,如果LHS和RHS都是null,则返回0

首先我们需要去做的就是先去判断出token,这样的话我们就要去为二元运算符增加enum类型,如果遇到binary关键字就返回相应的枚举类型

enum Token_Type {
    EOF_TOKEN = 0,
    NUMERIC_TOKEN,
    IDENTIFIER_TOKEN,
    LEFT_PARAN_TOKEN,
    RIGHT_PARAN_TOKEN,
    DEF_TOKEN,
    COMMA_TOKEN,
    IF_TOKEN,
    THEN_TOKEN,
    ELSE_TOKEN,
    FOR_TOKEN,
    IN_TOKEN,
    //二元运算符的标识符
    BINARY_TOKEN
};

相应的对于分析token的函数get_token()当中就增加上相应的二元运算符的识别

        //返回二元运算符
        if (Identifier_string=="binary") {

            return BINARY_TOKEN;
        }

以及由于我们是基于def进行操作的所有其实我们去定义二元运算符的AST结构的时候,只需要对函数定义的AST结构进行修改就可以了

class FunctionDeclAST
{
    //函数名
    std::string Func_Name;
    //参数列表
    std::vector<std::string> Arguments;
    //是否是运算符
    bool isOperator;

    //优先级
    unsigned Precedence;

    public:
    FunctionDeclAST(const std::string &name,const std::vector<std::string> &args,bool isoperator = false,unsigned prec = 0):Func_Name(name),Arguments(args),isOperator(isoperator),Precedence(prec){}
    //获取一元运算符的名字
    bool isUnaryOp() const {return isOperator && Arguments.size()==1;}
    //获取二元运算符的名字
    bool isBinaryOp() const {return isOperator && Arguments.size();}

    //获取操作符的名字
    char getOperatorName() const {
        assert(isUnaryOp() || isBinaryOp());
        return Func_Name[Func_Name.size()-1];
    }

    //返回优先级的函数
    unsigned getBinaryPrecedence() const {
        return Precedence;
    }
    Function *Codegen();
};

然后我们再去修改函数声明的解析器也就是func_decl_parser函数

static FunctionDeclAST *func_decl_parser()
{
    //定义的是函数的名字
    std::string FnName;

    //种类标识码
    unsigned Kind = 0;
    //运算符的优先级
    unsigned BinaryPrecedence = 30;

    //判断当前的token
    switch (Current_token) {
        default:
            return 0;
        //表示是标识符
        case IDENTIFIER_TOKEN:
            //设置标识符
            FnName = Identifier_string;
            //设置种类码
            Kind = 0;
            //读取下一个token
            next_token();
            break;
        //表示是二元标识符
        case BINARY_TOKEN:
            //读取下一个token
            next_token();
            //判断当前的token在不在ASCII码当中
            if (!isascii(Current_token))
                return 0;
            //设置函数名
            FnName = "binary";
            FnName += (char)Current_token;
            //设置种类码
            Kind = 2;
            //读取下一个token
            next_token();

            if (Current_token == NUMERIC_TOKEN) {
                if (Numeric_Val < 1 || Numeric_Val > 100)
                    return 0;
                //设置优先级
                BinaryPrecedence = (unsigned)Numeric_Val;
                next_token();
            }
            break;
    }

    //判断当前运算符是不是(,如果是的话就返回0
    if (Current_token != '(')
        return 0;

    std::vector<std::string> ArgNames;
    //判断是不是IDENTIFIER_TOKEN,如果是,就放入容器中
    while (next_token() == IDENTIFIER_TOKEN)
        ArgNames.push_back(Identifier_string);

    //判断标识符是不是)如果不是了
    if (Current_token != ')')
        return 0;

    //接着去判断下一个token
    next_token();

    //判断kind和参数的个数是不是一致的
    if (Kind && ArgNames.size() != Kind)
        return 0;

    return new FunctionDeclAST(FnName, ArgNames, Kind != 0, BinaryPrecedence);
}

之后我们再去修改二元AST的Codegen()函数也就是BinaryAST函数,我们其实在里面就是加了下面的内容

 //返回指令所处的函数
    /**
     LLVM Module的符号表中查找函数名。如前文所述,LLVM Module是个容器,待处理的函数全都在里面。只要保证各函数的名字与用户指定的函数名一致,我们就可以利用LLVM的符号表替我们完成函数名的解析
     */
    Function *F = Module_Ob->getFunction(std::string("binary") + Bin_Operator);
    assert(F && "binary operator not found!");

    Value *Ops[] = { L, R };
    //传入参数和函数以及指令名字,创建一条LLVM call指令
    return Builder.CreateCall(F, Ops, "binop");

由于我们可以在我们的语言当中设置了优先级,所以我们在函数声明需要去解析优先级问题,因为可能里面使用到了 | 运算,修改就是增加了下面的两段代码

 //根据函数声明的AST结构里面的BinaryOp设置
    if (Func_Decl->isBinaryOp()) {
        Operator_Precedence[Func_Decl->getOperatorName()] = Func_Decl->getBinaryPrecedence();
    }
    if(Func_Decl->isBinaryOp())
        Operator_Precedence.erase(Func_Decl->getOperatorName());

修改是在FunctionDefnAST::Codegen()函数中进行修改的,完整的代码如下所示

//函数定义
Function *FunctionDefnAST::Codegen() {

      /*生成函数原型(Proto)的代码并进行校验。与此同时,需要清空NamedValues映射表,确保其中不会残留之前代码生成过程中的产生的内容。函数原型的代码生成完毕后,一个现成的LLVM Function对象就到手了。*/
    Named_Values.clear();

    //生成函数定义的IR代码,
    Function *TheFunction = Func_Decl->Codegen();

    if(TheFunction == 0) return 0;

    //根据函数声明的AST结构里面的BinaryOp设置
    if (Func_Decl->isBinaryOp()) {
        Operator_Precedence[Func_Decl->getOperatorName()] = Func_Decl->getBinaryPrecedence();
    }
    /*
     模块(Module),函数(Function),代码块(BasicBlock),指令(Instruction)
     模块包含了函数,函数又包含了代码块,后者又是由指令组成。除了模块以外,所有结构都是从值产生而来的。
     */
    //新建了一个名为“entry”的基本块对象,稍后该对象将被插入TheFunction
    BasicBlock *BB = BasicBlock::Create(MyGlobalContext,"entry", TheFunction);

    //插入BasicBlock 代码块,告诉Builder,后续的新指令应该插至刚刚新建的基本块的末尾处。
    //builder.setInsertPoint 会告知 LLVM 引擎接下来将指令插入何处,简而言之,它指定创建的指令应该附加到指定块的末尾
    Builder.SetInsertPoint(BB);

    //现在该开始设置Builder对象了。LLVM基本块是用于定义控制流图(Control Flow Graph)的重要部件。当前我们还不涉及到控制流,所以所有的函数都只有一个基本块
    //函数体产生
    if(Value *RetVal = Body->Codegen()) {

        //创建一个ret指令,如果我们增加了if-then-else的话,这里返回的RetVal就是Phi类型的
        /**
         ifcont:                                           ; preds = %else, %then
         %iftmp = phi i32 [ 1, %then ], [ %addtmp, %else ]
         ret i32 %iftmp
         }
         */
        Builder.CreateRet(RetVal);
        //验证生成的代码,检查一致性。
        //简单检查一个函数的错误,在调试一个pass时有用。如果没有错误,函数返回false。如果发现错误,则向OS(如果非空)写入描述错误的消息,并返回true。
        verifyFunction(*TheFunction);
        //使用PassManager的run方法就可以进行优化

        //安排Pass进行执行,跟踪是否有任何传递修改函数,如果有,返回true。
       Global_FP->run(*TheFunction);

        return TheFunction;
    }
    //Error reading body, remove function.
    //错误读取函数体,所以删除函数
    TheFunction->eraseFromParent();

    if(Func_Decl->isBinaryOp())
        Operator_Precedence.erase(Func_Decl->getOperatorName());
    return 0;
}

接下来我们就去对最上面提到的代码进行生成IR指令,没有经过优化的结果如下所示

; ModuleID = 'my compiler'
source_filename = "my compiler"

define i32 @"binary|"(i32 %LHS, i32 %RHS) {
entry:
  %ifcond = icmp eq i32 %LHS, 0
  br i1 %ifcond, label %else, label %then

then:                                             ; preds = %entry
  br label %ifcont4

else:                                             ; preds = %entry
  %ifcond1 = icmp eq i32 %RHS, 0
  br i1 %ifcond1, label %else3, label %then2

then2:                                            ; preds = %else
  br label %ifcont

else3:                                            ; preds = %else
  br label %ifcont

ifcont:                                           ; preds = %else3, %then2
  %iftmp = phi i32 [ 1, %then2 ], [ 0, %else3 ]
  br label %ifcont4

ifcont4:                                          ; preds = %ifcont, %then
  %iftmp5 = phi i32 [ 1, %then ], [ %iftmp, %ifcont ]
  ret i32 %iftmp5
}

当我们增加了下面一句代码就可以进行优化,其实就是利用了LLVM当中的Pass

My_FP.add(createCFGSimplificationPass());

优化过后的生成的结果如下所示

; ModuleID = 'my compiler'
source_filename = "my compiler"

define i32 @"binary|"(i32 %LHS, i32 %RHS) {
entry:
  %ifcond = icmp eq i32 %LHS, 0
  %ifcond1 = icmp eq i32 %RHS, 0
  %. = select i1 %ifcond1, i32 0, i32 1
  %iftmp5 = select i1 %ifcond, i32 %., i32 1
  ret i32 %iftmp5
}

总结来说其实就是要去根据相应的binary的标识符然后识别出BINARY_TOKEN,然后由于我们这里定义的结构是def binary | (LHS RHS)以这样的形式定义的,而我们的函数则是以def foo(x,y)定义的,所以这里我们对函数声明的AST结构还是需要去重新定义,在这个结构当中去定义了getOperatorName()函数获取二元运算符的名字用于设置优先级,以及获取优先级的方法getBinaryPrecedence()

主要就是在

Operator_Precedence[Func_Decl->getOperatorName()]=Func_Decl->getBinaryPrecedence();中使用进行设置,之后在使用到 | 运算符的时候就会进行判断运算符的优先级

猜你喜欢

转载自blog.csdn.net/zcmuczx/article/details/80836062