基本表达式解析

我们先从数字开始,因为它们是最容易处理的。首先,我们先定义一个处理数字的函数:

  1. /// numberexpr ::= number
  2. static ExprAST *ParseNumberExpr() {
  3. ExprAST *Result = new NumberExprAST(NumVal);
  4. getNextToken(); // consume the number
  5. return Result;
  6. }

这一部分代码很简单:若当前的token是一个指向数字的tok_number,则调用ParseNumberExpr,它会读取当前数值,创建NumberExprAST节点,然后读取下一token,以便接下来的解析,最后,返回结果。

这其中还有一些有趣的东西。最重要的一点是,这些解析节点的代码会将所有与之相关的token都读取掉,同时在返回结果前会再次调用getNextToken来清除掉当前的token,得到下一个token(通常这个token不属于当前节点)。这在递归下降解析器中是一个普遍的做法。下面给出一个例子可以更好地理解,这个例子是关于解析一对括号的:

  1. /// parenexpr ::= '(' expression ')'
  2. static ExprAST *ParseParenExpr() {
  3. getNextToken(); // eat (.
  4. ExprAST *V = ParseExpression();
  5. if (!V) return 0;
  6.  
  7. if (CurTok != ')')
  8. return Error("expected ')'");
  9. getNextToken(); // eat ).
  10. return V;
  11. }

这个函数演示了几个关于解析器的有趣的方面:

  • 异常检测:当被调用时,这个函数会默认当前的token是(,但是当结束表达式解析后,有可能末尾的token就不是)。比如,如果用户错将(4)打成了(4 *,解析器就会检测到这个错误,为了提醒有错误发生,我们的解析器将返回NULL。
  • 递归式解析:这段函数中调用了ParseExpression(我们将很快看到ParseExpression同样会调用ParseParenExpr)。这种方式相当强大,因为它允许我们处理嵌套的语法,同时也保持了每一个过程都是相当简洁。注意,括号并不会成为抽象语法树的组成部分,它的作用是将表达式组合起来引导引导解析器正确地处理它们。当建立好了抽象语法树后,它们便可以被抛弃了。
    下一步我们来写变量的解析器:
  1. /// identifierexpr
  2. /// ::= identifier
  3. /// ::= identifier '(' expression* ')'
  4. static ExprAST *ParseIdentifierExpr() {
  5. std::string IdName = IdentifierStr;
  6.  
  7. getNextToken(); // eat identifier.
  8.  
  9. if (CurTok != '(') // Simple variable ref.
  10. return new VariableExprAST(IdName);
  11.  
  12. // Call.
  13. getNextToken(); // eat (
  14. std::vector<ExprAST*> Args;
  15. if (CurTok != ')') {
  16. while (1) {
  17. ExprAST *Arg = ParseExpression();
  18. if (!Arg) return 0;
  19. Args.push_back(Arg);
  20.  
  21. if (CurTok == ')') break;
  22.  
  23. if (CurTok != ',')
  24. return Error("Expected ')' or ',' in argument list");
  25. getNextToken();
  26. }
  27. }
  28.  
  29. // Eat the ')'.
  30. getNextToken();
  31.  
  32. return new CallExprAST(IdName, Args);
  33. }

这段解析代码和其它的很类似。若当前token为tok_identifier时,该函数被调用。同样具有递归的解析思想,和同样的错误处理方法。有趣的一点是,这里还用到了一个前置判断(look-ahead)来决定当前的identifier是一个函数调用,还是一个变量。判断的方法是读取下一个token,若下一个token不是(,则这是函数调用这时候返回VariableExprAST,否则是使用变量,返回CallExprAST

现在我们所有的简单表达式解析器代码已经就位,我们可以定义一个辅助函数来包装并调用它们。我们把目前我们完成的简单的表达式取名为基本表达式(primary expressions),到后面你就会更加理解这个名字了。以下就是基本表达式解析器:

  1. /// primary
  2. /// ::= identifierexpr
  3. /// ::= numberexpr
  4. /// ::= parenexpr
  5. static ExprAST *ParsePrimary() {
  6. switch (CurTok) {
  7. default: return Error("unknown token when expecting an expression");
  8. case tok_identifier: return ParseIdentifierExpr();
  9. case tok_number: return ParseNumberExpr();
  10. case '(': return ParseParenExpr();
  11. }
  12. }

通过基本表达式解析器,我们可以明白为什么我们要使用CurTok了,这里用了前置判断来选择并调用解析器。

现在基本的表达式解析器已经完成了,我们下一步开始处理二元表达式,这会有一点复杂。