语法分析器(Parser)的实现

  • 前言

    语法分析器实现参考自文章:

    (https://llvm-tutorial-cn.readthedocs.io/en/latest/chapter-2.html)

  • 抽象语法树的定义

    抽象语法树的作用在于牢牢抓住程序的脉络,从而方便编译过程的后续环节(如代码生成)对程序进行解读。

    我们可以分别定义出所有语法结构的抽象语法树。

    我们先定义一个基类,并让其他的类继承于它:

    1
    2
    3
    4
    class ExprAST{
    public:
    virtual ~ExprAST(){}
    };

    对于常量的抽象语法树的定义如下:

    1
    2
    3
    4
    5
    class NumExprAST : public ExprAST{
    double __Val;
    public:
    NumExprAST(double val): __Val(val) {}
    };

    当然,对于其他语法结构,如二元运算符,变量,函数等语法结构的定义也大同小异:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class VarExprAST : public ExprAST{
    std::string __Name;
    public:
    VarExprAST(const string &name) : __Name(name) {}
    };

    class BinExprAST : public ExprAST{
    char __op;
    ExprAST *__LHS, *__RHS;
    public:
    BinExprAST(char op,ExprAST *lhs, ExprAST *rhs) : __op(op),__LHS(lhs),__RHS(rhs) {}
    };

    class CallExprAST : public ExprAST{
    std::string __Callee;
    std::vector<ExprAST*> __Args;
    public:
    CallExprAST(std::string &callee, std::vector<ExprAST*> Args) : __Callee(callee),__Args(Args) {}
    };

    这里你可能会比较好奇为什么在二元运算符的抽象语法树中,定义了两个ExprAST类型的指针呢,我们来看一个例子:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /*假如有如下表达式
    x + y;
    那么我们要表示他需要创建三个抽象语法树的对象:
    1.x ==> variable ==> ExprAST *X = new VarExprAST("X");
    2.y ==> variable ==> ExprAST *Y = new VarExprAST("Y");
    前两个我已经给出了,由于x与y是变量,所以是VarExprAST类的,但是当我们整体来看表达式:x+y,他是属于BinExprAST的,当然x与y是属于VarExprAST的,我们令Reslut = X+Y,即可得出以下表达式:
    3.Result ==> Binary Opcode ==> ExprAST *Result = new BinExprAST("+",X,Y);
    这就可以看出为什么有两个ExprAST类型的指针,因为他们分别执行运算符两端的数据:X与Y。
    */
    ExprAST *X = new VariableExprAST("x");
    ExprAST *Y = new VariableExprAST("y");
    ExprAST *Result = new BinaryExprAST('+', X, Y);
  • 语法解析器

    我们先从一些简单的辅助函数开始定义:

    1
    2
    3
    4
    static int CurTok;
    static int getNextToken() {
    return CurTok = gettok();
    }

    这段代码以词法分析器为中心,实现了一个简易的语元缓冲,让我们能够预先读取词法分析器将要返回的下一个语元。在我们的语法解析器中,所有函数都将CurTok视作当前待解析的语元。

    再定义三个用于报错的函数:

    1
    2
    3
    ExprAST *Error(const char *Str) { fprintf(stderr, "Error: %s\n", Str);return 0;}
    PrototypeAST *ErrorP(const char *Str) { Error(Str); return 0; }
    FunctionAST *ErrorF(const char *Str) { Error(Str); return 0; }

    定义好这三个函数之后,我们就可以开始解析我们的表达式了

  • 解析基本表达式

    我们先从常量开始解析,解析常量的函数很简单:调用它的时候,当前待解析语元只能是tok_number。该函数用刚解析出的数值构造出了一个NumberExprAST节点,然后令词法分析器继续读取下一个语元,最后返回构造的AST节点。

    1
    2
    3
    4
    5
    static ExprAST *ParseNumberExpr() {
    ExprAST *Result = new NumberExprAST(NumVal);
    getNextToken();
    return Result;
    }

    接下来解析变量与函数的调用,该函数与其它函数区别不大。(调用该函数时当前语元必须是tok_identifier。)这里采用了预读(lookahead)的手段来试探当前标识符的类型,判断它究竟是个独立的变量引用还是个函数调用。只要检查紧跟标识符之后的语元是不是“(”,它就能知道到底应该构造VarExprAST节点还是CallExprAST节点。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    static ExprAST *ParseIdentifierExpr() {
    std::string IdName = IdentifierStr;

    getNextToken();

    if (CurTok != '(')
    return new VariableExprAST(IdName);

    getNextToken();
    std::vector<ExprAST*> Args;
    if (CurTok != ')') {
    while (1) {
    ExprAST *Arg = ParseExpression();
    if (!Arg) return 0;
    Args.push_back(Arg);

    if (CurTok == ')') break;

    if (CurTok != ',')
    return Error("Expected ')' or ',' in argument list");
    getNextToken();
    }
    }

    getNextToken();

    return new CallExprAST(IdName, Args);
    }

    到这里,基本表达式全都搞定了,下面开始开始着手解析更为复杂的二元表达式。

  • 解析二元表达式(来自参考文章)

    二元表达式的解析难度要大得多,因为它们往往具有二义性。例如,给定字符串“x+y*z”,语法解析器既可以将之解析为“(x+y)*z”,也可以将之解析为“x+(y*z)”。按照通常的数学定义,我们期望解析成后者,因为“*”(乘法)的优先级要高于“+”(加法)。

    这个问题的解法很多,其中属运算符优先级解析最为优雅和高效。这是一种利用二元运算符的优先级来引导递归调用走向的解析技术。首先,我们需要制定一张优先级表:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    /// BinopPrecedence - This holds the precedence for each binary operator that is
    /// defined.
    static std::map<char, int> BinopPrecedence;

    /// GetTokPrecedence - Get the precedence of the pending binary operator token.
    static int GetTokPrecedence() {
    if (!isascii(CurTok))
    return -1;

    // Make sure it's a declared binop.
    int TokPrec = BinopPrecedence[CurTok];
    if (TokPrec <= 0) return -1;
    return TokPrec;
    }

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

    函数GetTokPrecedence用于查询当前语元的优先级,如果当前语元不是二元运算符则返回-1。这里的map简化了新运算符的添加,同时也可以证明我们的算法与具体的运算符无关。当然,要想去掉map直接在GetTokPrecedence中比较优先级也很简单。(甚至可以直接使用定长数组)。

    有了上面的函数作为辅助,我们就可以开始解析二元表达式了。运算符优先级解析的基本思想就是通过拆解含有二元运算符的表达式来解决可能的二义性问题。以表达式“a+b+(c+d)*e*f+g”为例,在进行运算符优先级解析时,它将被视作一串按二元运算符分隔的主表达式。按照这个思路,解析出来的第一个主表达式应该是“a”,紧跟着是若干个有序对,即:[+, b][+, (c+d)][*, e][*, f][+, g]。注意,括号表达式也是主表达式,所以在解析二元表达式时无须特殊照顾(c+d)这样的嵌套表达式。

    一开始,每个表达式都由一个主表达式打头阵,身后可能还跟着一串由有序对构成的列表,其中有序对的格式为[binop, primaryexpr]

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /// expression
    /// ::= primary binoprhs
    ///
    static ExprAST *ParseExpression() {
    ExprAST *LHS = ParsePrimary();
    if (!LHS) return 0;

    return ParseBinOpRHS(0, LHS);
    }

    函数ParseBinOpRHS用于解析有序对列表(其中RHS是Right Hand Side的缩写,表示“右侧”;与此相对应,LHS表示“左侧”——译者注)。它的参数包括一个整数和一个指针,其中整数代表运算符优先级,指针则指向当前已解析出来的那部分表达式。注意,单独一个“x”也是合法的表达式:也就是说binoprhs有可能为空;碰到这种情况时,函数将直接返回作为参数传入的表达式。在上面的例子中,传入ParseBinOpRHS的表达式是“a”,当前语元是“+”。

    传入ParseBinOpRHS的优先级表示的是该函数所能处理的最低运算符优先级。假设语元流中的下一对是“[+, x]”,且传入ParseBinOpRHS的优先级是40,那么该函数将直接返回(因为“+”的优先级是20)。搞清楚这一点之后,我们再来看ParseBinOpRHS的定义,函数的开头是这样的:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /// binoprhs
    /// ::= ('+' primary)*
    static ExprAST *ParseBinOpRHS(int ExprPrec, ExprAST *LHS) {
    // If this is a binop, find its precedence.
    while (1) {
    int TokPrec = GetTokPrecedence();

    // If this is a binop that binds at least as tightly as the current binop,
    // consume it, otherwise we are done.
    if (TokPrec < ExprPrec)
    return LHS;

    这段代码首先检查当前语元的优先级,如果优先级过低就直接返回。由于无效语元(这里指不是二元运算符的语元)的优先级都被判作-1,因此当语元流中的所有二元运算符都被处理完毕时,该检查自然不会通过。如果检查通过,则可知当前语元一定是二元运算符,应该被纳入当前表达式:

    1
    2
    3
    4
    5
    6
    7
    // Okay, we know this is a binop.
    int BinOp = CurTok;
    getNextToken(); // eat binop

    // Parse the primary expression after the binary operator.
    ExprAST *RHS = ParsePrimary();
    if (!RHS) return 0;

    就这样,二元运算符处理完毕(并保存妥当)之后,紧跟其后的主表达式也随之解析完毕。至此,本例中的第一对有序对[+, b]就构造完了。

    现在表达式的左侧和RHS序列中第一对都已经解析完毕,该考虑表达式的结合次序了。路有两条,要么选择“(a+b) binop unparsed”,要么选择“a + (b binop unparsed)”。为了搞清楚到底该怎么走,我们先预读出“binop”,查出它的优先级,再将之与BinOp(本例中是“+”)的优先级相比较:

    1
    2
    3
    4
    // If BinOp binds less tightly with RHS than the operator after RHS, let
    // the pending operator take RHS as its LHS.
    int NextPrec = GetTokPrecedence();
    if (TokPrec < NextPrec) {

    binop位于“RHS”的右侧,如果binop的优先级低于或等于当前运算符的优先级,则可知括号应该加在前面,即按“(a+b) binop ...”处理。在本例中,当前运算符是“+”,下一个运算符也是“+”,二者的优先级相同。既然如此,理应按照“a+b”来构造AST节点,然后我们继续解析:

    1
    2
    3
    4
    5
    6
    7
          // ... if body omitted ...
    }

    // Merge LHS/RHS.
    LHS = new BinaryExprAST(BinOp, LHS, RHS);
    } // loop around to the top of the while loop.
    }

    接着上面的例子,“a+b+”的前半段被解析成了“(a+b)”,于是“+”成为当前语元,进入下一轮迭代。上述代码进而将“(c+d)”识别为主表达式,并构造出相应的有序对[+, (c+d)]。现在,主表达式右侧的binop是“*”,由于“*”的优先级高于“+”,负责检查运算符优先级的if判断通过,执行流程得以进入if语句的内部。

    现在关键问题来了:if语句内的代码怎样才能完整解析出表达式的右半部分呢?尤其是,为了构造出正确的AST,变量RHS必须完整表达“(c+d)*e*f”。出人意料的是,写成代码之后,这个问题出奇地简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
        // If BinOp binds less tightly with RHS than the operator after RHS, let
    // the pending operator take RHS as its LHS.
    int NextPrec = GetTokPrecedence();
    if (TokPrec < NextPrec) {
    RHS = ParseBinOpRHS(TokPrec+1, RHS);
    if (RHS == 0) return 0;
    }

    // Merge LHS/RHS.
    LHS = new BinaryExprAST(BinOp, LHS, RHS);
    }
    }

    看一下主表达式右侧的二元运算符,我们发现它的优先级比当前正在解析的binop的优先级要高。由此可知,如果自binop以右的若干个连续有序对都含有优先级高于“+”的运算符,那么就应该把它们全部解析出来,拼成“RHS”后返回。为此,我们将最低优先级设为“TokPrec+1”,递归调用函数“ParseBinOpRHS”。该调用会完整解析出上述示例中的“(c+d)*e*f”,并返回构造出的AST节点,这个节点就是“+”表达式右侧的RHS

    最后,while循环的下一轮迭代将会解析出剩下的“+g”并将之纳入AST。仅需区区14行代码,我们就完整而优雅地实现了通用的二元表达式解析算法。

  • 解析其余结构

    下面来解析函数原型。在Kaleidoscope语言中,有两处会用到函数原型:一是“extern”函数声明,二是函数定义。相关代码很简单:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    /// prototype
    /// ::= id '(' id* ')'
    static PrototypeAST *ParsePrototype() {
    if (CurTok != tok_identifier)
    return ErrorP("Expected function name in prototype");

    std::string FnName = IdentifierStr;
    getNextToken();

    if (CurTok != '(')
    return ErrorP("Expected '(' in prototype");

    std::vector<std::string> ArgNames;
    while (getNextToken() == tok_identifier)
    ArgNames.push_back(IdentifierStr);
    if (CurTok != ')')
    return ErrorP("Expected ')' in prototype");

    // success.
    getNextToken(); // eat ')'.

    return new PrototypeAST(FnName, ArgNames);
    }

    在此基础之上,函数定义就很简单了,说白了就是一个函数原型再加一个用作函数体的表达式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /// definition ::= 'def' prototype expression
    static FunctionAST *ParseDefinition() {
    getNextToken(); // eat def.
    PrototypeAST *Proto = ParsePrototype();
    if (Proto == 0) return 0;

    if (ExprAST *E = ParseExpression())
    return new FunctionAST(Proto, E);
    return 0;
    }

    除了用于用户自定义函数的前置声明,“extern”语句还可以用来声明“sin”、“cos”等(C标准库)函数。这些“extern”语句不过就是些不带函数体的函数原型罢了:

    1
    2
    3
    4
    5
    /// external ::= 'extern' prototype
    static PrototypeAST *ParseExtern() {
    getNextToken(); // eat extern.
    return ParsePrototype();
    }

    最后,我们还允许用户随时在顶层输入任意表达式并求值。这一特性是通过一个特殊的匿名零元函数(没有任何参数的函数)实现的,所有顶层表达式都定义在这个函数之内:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    最后,我们还允许用户随时在顶层输入任意表达式并求值。这一特性是通过一个特殊的匿名零元函数(没有任何参数的函数)实现的,所有顶层表达式都定义在这个函数之内:

    /// toplevelexpr ::= expression
    static FunctionAST *ParseTopLevelExpr() {
    if (ExprAST *E = ParseExpression()) {
    // Make an anonymous proto.
    PrototypeAST *Proto = new PrototypeAST("", std::vector<std::string>());
    return new FunctionAST(Proto, E);
    }
    return 0;
    }
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2023 h1J4cker
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信