之前我们学习了词法分析器,接下来,依据词法分析器的输出,我们将进一步对代码进行处理。这里我们用到的是语法分析器,Parser。Parser的输出是一个抽象语法树(Abstract Syntax Tree,AST)。
Parser的构建采用的是两种方法的结合,Recursive Descent Parsing(递归下降解析)和Operator-Precedence Parsing(运算符优先解析)。后者仅用于二元表达式(Binary Expression),而其他情况使用前者。
递归下降解析是指一种自上而下的解析器,由一组相互递归的程序(或等价的非递归程序)构建而成,其中每个程序都实现了文法中的一个非终结符。因此,这些程序的结构密切反映了它所识别的文法结构。–wiki百科
运算符优先解析是一个自下而上的解析过程,它解释运算符先验的语法。很多计算器使用的是运算符有限解析。–wiki百科
由于parser的输出是AST,我们首先介绍一下AST。
抽象语法树,AST 一段程序的AST捕捉了这段程序的行为,从而可以为后续的编译过程(例如代码生成,code generation)提供便利。AST应该尽量紧密的模拟语言,对于语言中的每一个结构,我们都在AST中有对应的对象。在K语言中,我们有表达式expression,原型prototype和函数对象function object。
简单起见,我们将所有类的成员都设置为了public。先从表达式父类和它的子类开始。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 class ExprAST {public : virtual ~ExprAST () = default ; };class NumberExprAST : public ExprAST {public : double val; explicit NumberExprAST (double val) : val(val) { } };class VariableExprAST : public ExprAST {public : std::string name; explicit VariableExprAST (std::string name) : name(std::move(name)) { } };class BinaryExprAST : public ExprAST {public : char op; std::unique_ptr<ExprAST> lhs, rhs; BinaryExprAST (char op, std::unique_ptr<ExprAST> lhs, std::unique_ptr<ExprAST> rhs) : op (op), lhs (std::move (lhs)), rhs (std::move (rhs)) {} };class CallExprAST : public ExprAST {public : std::string callee; std::vector<std::unique_ptr<ExprAST>> args; CallExprAST (std::string callee, std::vector<std::unique_ptr<ExprAST>> args) : callee (std::move (callee)), args (std::move (args)) {} };class CommentAST : public ExprAST {public : std::string content; explicit CommentAST (std::string content) : content(std::move(content)) { } };
上面这些都十分简单,就是表达式类型对应的类,类中有相应的成员变量和构造函数。
除了条件控制表达式外,我们所有的表达式都进行了定义。缺少了条件控制表达式,这个语言到现在还不是图灵完备的。之后我们会将其加上,接下来我们将介绍一个函数的接口和函数自己。
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 29 30 31 32 33 34 35 36 class PrototypeAST {public : std::string name; std::vector<std::string> args; PrototypeAST (std::string name, std::vector<std::string> args) : name (std::move (name)), args (std::move (args)) {} [[nodiscard]] const std::string& get_name () const { return name; } std::string get_info () { std::string s = "name: " + this ->name + ", args:" ; for (const std::string& arg: args) s.append (" " + arg); return s; } };class FunctionAST {public : std::unique_ptr<PrototypeAST> proto; std::unique_ptr<ExprAST> body; FunctionAST (std::unique_ptr<PrototypeAST> proto, std::unique_ptr<ExprAST> body) : proto (std::move (proto)), body (std::move (body)) {} };
在目前的语言定义中,由于我们的变量统一为双精度浮点值,我们无需指定每个参数的类型,并且函数的类型由它的参数的个数而决定。到这里,我们定义了所有的AST节点类,接下来我们就需要解析代码为一个个AST节点。
语法解析器,Parser 简单的帮助函数 下面的cur_token和get_next_token提供了最简单的token缓冲区,cur_token是当前parser正在处理的token,get_next_token从lexer处读取下一个token,并将返回的结果赋值给cur_token。
1 2 3 4 5 static int cur_token;static int get_next_token () { return cur_token = get_tok (); }
然后再定义一些简单的错误处理函数。
1 2 3 4 5 6 7 8 9 10 std::unique_ptr<ExprAST> log_error (const char * str) { std::fprintf (stderr, "LogError: %s\n" , str); return nullptr ; }std::unique_ptr<PrototypeAST> log_error_p (const char * str) { log_error (str); return nullptr ; }
Primary表达式 接下来就开始实现语法解析器,首先实现的是数字literal,当lexer返回tok_number时调用。
1 2 3 4 5 6 7 8 9 10 static std::unique_ptr<ExprAST> parse_number_expr () { auto result = std::make_unique <NumberExprAST>(num_val); get_next_token (); return std::move (result); }
然后是小括号的解析,小括号表达式是指用小括号将一个表达式包裹起来,当lexer返回的是前小括号(时调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 static std::unique_ptr<ExprAST> parse_paren_expr () { get_next_token (); auto v = parse_expression (); if (!v) return nullptr ; if (cur_token != ')' ) return log_error ("Expect ')'" ); get_next_token (); return v; }
值得注意的是,在函数的第二行,我们调用了parse_expression(),这个函数是用来解析一个表达式的,我们还没有定义。parse_expression()和parse_paren_expr()是递归调用的关系,他们相互调用。当前者遇到前小括号,则调用后者,后者在处理括号中的表达式时,会调用前者。此外,我们还可以发现,小括号并不会构建任何AST节点,parse_paren_expr()返回的也是括号中的表达式节点。小括号的作用是引导parser进行AST的构建,有时它会制定解析时的优先顺序,一旦括号中的表达式解析完成,就不需要小括号了。
下面对标识符进行解析,当lexer返回tok_identifier时进行调用,标识符可以分为变量的引用和函数的调用。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 static std::unique_ptr<ExprAST> parse_identifier_expr () { std::string id_name = identifier_str; get_next_token (); if (cur_token != '(' ) return std::make_unique <VariableExprAST>(id_name); get_next_token (); std::vector<std::unique_ptr<ExprAST>> args; if (cur_token != ')' ) { while (true ) { if (auto arg = parse_expression ()) args.push_back (std::move (arg)); else return nullptr ; if (cur_token == ')' ) break ; if (cur_token != ',' ) return log_error ("Expect ',' or ')' in argument list" ); get_next_token (); } } get_next_token (); return std::make_unique <CallExprAST>(id_name, std::move (args)); }
下面是注释。
1 2 3 4 5 static std::unique_ptr<CommentAST> parse_comment () { get_next_token (); return std::make_unique <CommentAST>(std::move (comment_line)); }
值得注意的是,这里,我们通过判断当前位置的下一个字符是否是前括号,来判断当前的标识符是变量引用还是函数调用。到这里,我们已经完成了所有的简单表达式的解析函数,我们接着用一个大的helper函数将他们都打包在一起。我们将这些表达式统称为”primary”表达式,然后实现以下的函数。
此外,注释是我个人添加进去的,它不算primary表达式,但是为了简单,我在下面将它的解析也打包进了函数中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 static std::unique_ptr<ExprAST> parse_primary () { if (cur_token == tok_identifier) return parse_identifier_expr (); if (cur_token == tok_number) return parse_number_expr (); if (cur_token == '(' ) return parse_paren_expr (); if (cur_token == tok_comment) return parse_comment (); std::string msg = "Unknown token when expecting an expression: " ; msg += std::to_string (cur_token); msg += " " ; msg += char (cur_token); return log_error (msg.c_str ()); }
二进制表达式 接下来是二元表达式的解析了,这里明显会更难,因为二元表达式经常有歧义。例如a+b*c,我们可以先计算加法,再进行乘法,也可以换过来,但是根据运算符的优先级,先计算乘法是正确的。解决这一问题的方法有很多,其中最优雅的方式是用运算符优先解析方法(Operator-Precedence Parsing),该方法通过运算符的优先级来指导递归的进程,首先我们需要先定义所有可能的运算符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 static std::map<char , int > bin_op_precedence = { {'<' , 10 }, {'+' , 20 }, {'-' , 20 }, {'*' , 40 } };static int get_tok_precedence () { if (!isascii (cur_token)) return -1 ; int tok_prec = bin_op_precedence[char (cur_token)]; if (tok_prec <= 0 ) return -1 ; return tok_prec; }
我们现在可以开始解析二元表达式,解析的基本想法是将表达式按运算符拆解开来。例如,”a+b+(c+d)e f+g”,我们首先将他们看做是一系列被运算符隔开的primary表达式。因此,解析器首先解析最前面的a,然后会看到一系列pair,即[+, b] [+, (c+d)] [, e] [ , f]和[+, g]。注意到,由于小括号是primary表达式,我们无需考虑括号内的+号与其他运算符的优先级关系,首先,一个表达式是一个可能后面跟着一系列[binop, primaryexpr] pair的primary表达式。
1 2 3 4 5 6 7 8 9 10 11 static std::unique_ptr<ExprAST> parse_bin_op_rhs (int expr_prec, std::unique_ptr<ExprAST> lhs) ;static std::unique_ptr<ExprAST> parse_expression () { auto lhs = parse_primary (); if (!lhs) return nullptr ; return parse_bin_op_rhs (0 , std::move (lhs)); }
parse_bin_op_rhs是用来解析[binop, primaryexpr] pair的函数,它的参数是一个优先级,和一个指向目前为止已经解析部分的表达式的指针。值得注意的是,”x”也是一个有效的表达式,即后面没有任何pair,因此binoprhs可以为空,在这种情况下,函数直接返回表达式。在上面的”a+b+(c+d)e f+g”的表达式中,上面的parse_expression函数会先解析a,然后把当前的读取位置移到”+”上,然后调用这个函数,传入的参数是0和指向a表达式的指针。
传入该函数的优先级expr_prec表示该函数中,允许解析的运算符的最低优先级。例如,如果当前的pair是[+, b],而此时传入的优先级参数是40,那么这个函数将不会解析这个pair,因为+的优先级是20,下面的函数中,我们将使用”a+b+(c+d)e f+g”作为说明是的例子。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 static std::unique_ptr<ExprAST> parse_bin_op_rhs (int expr_prec, std::unique_ptr<ExprAST> lhs) { while (true ) { int tok_prec = get_tok_precedence (); if (tok_prec < expr_prec) return lhs; int bin_op = cur_token; get_next_token (); auto rhs = parse_primary (); if (!rhs) return nullptr ; int next_prec = get_tok_precedence (); if (tok_prec < next_prec) { rhs = parse_bin_op_rhs (tok_prec + 1 , std::move (rhs)); if (!rhs) return nullptr ; } lhs = std::make_unique <BinaryExprAST>(bin_op, std::move (lhs), std::move (rhs)); } }
下面我模拟了一下整个过程。
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 29 30 31 32 33 34
函数定义 下面是函数定义的解析,由于我们只有双精度浮点数,我们不需要给变量指定类型,参数之间用空格隔开就行了。
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 29 30 31 static std::unique_ptr<PrototypeAST> parse_prototype () { if (cur_token != tok_identifier) return log_error_p ("Expect function name in prototype" ); std::string fn_name = identifier_str; get_next_token (); if (cur_token != '(' ) return log_error_p ("Expect '(' in prototype" ); std::vector<std::string> arg_names; while (get_next_token () == tok_identifier) arg_names.push_back (identifier_str); if (cur_token != ')' ) log_error_p ("Expect ')' in prototype" ); get_next_token (); return std::make_unique <PrototypeAST>(fn_name, std::move (arg_names)); }
有了prototype,function definition就很简单了,就是prototype加上function body。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 static std::unique_ptr<FunctionAST> parse_definition () { get_next_token (); auto proto = parse_prototype (); if (!proto) return nullptr ; if (auto e = parse_expression ()) return std::make_unique <FunctionAST>(std::move (proto), std::move (e)); return nullptr ; }
顶层top-level解析 最后,我们还将让用户输入任意的顶层表达式,并对它们进行实时评估,我们将通过为它们定义匿名的nullary(零参数)函数来处理这个问题。我的理解就是从文件的角度,我们要将从文件视角看下去的元素进行解析,而不是函数角度。
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 29 30 static std::unique_ptr<FunctionAST> parse_top_level_expr () { if (auto e = parse_expression ()) { auto proto = std::make_unique <PrototypeAST>("" , std::vector <std::string>()); return std::make_unique <FunctionAST>(std::move (proto), std::move (e)); } return nullptr ; }static std::unique_ptr<PrototypeAST> parse_extern () { get_next_token (); return parse_prototype (); }static std::unique_ptr<CommentAST> parse_top_level_comment () { get_next_token (); return std::make_unique <CommentAST>(std::move (comment_line)); }
下面是从文件角度来进行解析的函数。
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 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 static void handle_definition () { unique_ptr<FunctionAST> fun = parse_definition (); if (fun) { std::fprintf (stderr, "Parsed a function definition, %s\n" , fun->proto->get_info ().c_str ()); } else { std::fprintf (stderr, "Error when handling function definition, skip\n" ); get_next_token (); } }static void handle_extern () { unique_ptr<PrototypeAST> ext = parse_extern (); if (parse_extern ()) { std::fprintf (stderr, "Parsed a extern, %s\n" , ext->get_info ().c_str ()); } }static void handle_top_level_expression () { if (parse_top_level_expr ()) { std::fprintf (stderr, "Parsed a top-level expr.\n" ); } else { std::fprintf (stderr, "Error when handling top-level expression, skip\n" ); get_next_token (); } }static void handle_top_level_comment () { unique_ptr<CommentAST> comment = parse_top_level_comment (); if (comment) std::fprintf (stderr, "Parsed a comment, content: %s\n" , comment->content.c_str ()); else { std::fprintf (stderr, "Error when handling top-level comment, skip\n" ); get_next_token (); } }
简单的driver 我们写一个简单的driver,判断lexer返回的token类型,调用相对应的处理函数。
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 29 30 int main_loop () { while (true ) { fprintf (stderr, "ready> " ); if (cur_token == tok_eof) return 0 ; if (cur_token == ';' ) { get_next_token (); } else if (cur_token == tok_def) { handle_definition (); } else if (cur_token == tok_extern) { handle_extern (); } else if (cur_token == tok_comment) { handle_top_level_comment (); } else { handle_top_level_expression (); } } }
这样我们就可以通过每次输入一个表达式,得到解析器解析的结果。
1 2 ready> def foo(x y z) x + y * z Parsed a function definition, name: foo, args: x y z
总结 我们到现在为止可以分析出代码中的各种成分,并能判断代码是否是语法正确的。下面,我们将介绍如何从AST生成LLVM IR。