基本表达式解析
我们先从数字开始,因为它们是最容易处理的。首先,我们先定义一个处理数字的函数:
- /// numberexpr ::= number
- static ExprAST *ParseNumberExpr() {
- ExprAST *Result = new NumberExprAST(NumVal);
- getNextToken(); // consume the number
- return Result;
- }
这一部分代码很简单:若当前的token是一个指向数字的tok_number
,则调用ParseNumberExpr
,它会读取当前数值,创建NumberExprAST
节点,然后读取下一token,以便接下来的解析,最后,返回结果。
这其中还有一些有趣的东西。最重要的一点是,这些解析节点的代码会将所有与之相关的token都读取掉,同时在返回结果前会再次调用getNextToken
来清除掉当前的token,得到下一个token(通常这个token不属于当前节点)。这在递归下降解析器中是一个普遍的做法。下面给出一个例子可以更好地理解,这个例子是关于解析一对括号的:
- /// parenexpr ::= '(' expression ')'
- static ExprAST *ParseParenExpr() {
- getNextToken(); // eat (.
- ExprAST *V = ParseExpression();
- if (!V) return 0;
- if (CurTok != ')')
- return Error("expected ')'");
- getNextToken(); // eat ).
- return V;
- }
这个函数演示了几个关于解析器的有趣的方面:
- 异常检测:当被调用时,这个函数会默认当前的token是(,但是当结束表达式解析后,有可能末尾的token就不是)。比如,如果用户错将(4)打成了(4 *,解析器就会检测到这个错误,为了提醒有错误发生,我们的解析器将返回NULL。
- 递归式解析:这段函数中调用了ParseExpression(我们将很快看到ParseExpression同样会调用ParseParenExpr)。这种方式相当强大,因为它允许我们处理嵌套的语法,同时也保持了每一个过程都是相当简洁。注意,括号并不会成为抽象语法树的组成部分,它的作用是将表达式组合起来引导引导解析器正确地处理它们。当建立好了抽象语法树后,它们便可以被抛弃了。
下一步我们来写变量的解析器:
- /// identifierexpr
- /// ::= identifier
- /// ::= identifier '(' expression* ')'
- static ExprAST *ParseIdentifierExpr() {
- std::string IdName = IdentifierStr;
- getNextToken(); // eat identifier.
- if (CurTok != '(') // Simple variable ref.
- return new VariableExprAST(IdName);
- // Call.
- getNextToken(); // eat (
- 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();
- }
- }
- // Eat the ')'.
- getNextToken();
- return new CallExprAST(IdName, Args);
- }
这段解析代码和其它的很类似。若当前token为tok_identifier
时,该函数被调用。同样具有递归的解析思想,和同样的错误处理方法。有趣的一点是,这里还用到了一个前置判断(look-ahead)来决定当前的identifier是一个函数调用,还是一个变量。判断的方法是读取下一个token,若下一个token不是(
,则这是函数调用这时候返回VariableExprAST
,否则是使用变量,返回CallExprAST
。
现在我们所有的简单表达式解析器代码已经就位,我们可以定义一个辅助函数来包装并调用它们。我们把目前我们完成的简单的表达式取名为基本表达式(primary expressions),到后面你就会更加理解这个名字了。以下就是基本表达式解析器:
- /// primary
- /// ::= identifierexpr
- /// ::= numberexpr
- /// ::= parenexpr
- static ExprAST *ParsePrimary() {
- switch (CurTok) {
- default: return Error("unknown token when expecting an expression");
- case tok_identifier: return ParseIdentifierExpr();
- case tok_number: return ParseNumberExpr();
- case '(': return ParseParenExpr();
- }
- }
通过基本表达式解析器,我们可以明白为什么我们要使用CurTok
了,这里用了前置判断来选择并调用解析器。
现在基本的表达式解析器已经完成了,我们下一步开始处理二元表达式,这会有一点复杂。