C编译器剖析_1.3 由文法到分析器_表达式Expression

1.3 由文法到分析器

     学英语时,我们从基本的26个英文字母入手,然后由英文字母组成各种各样的单词,之后按照语法规则组成句子,之后是段落,然后是文章。同理,我们要写一个语法分析器来识别上节引入的文法对应的语言时,也需要一个字母表,其中包括我们要定义的语言的基本字符,例如阿拉伯数字、英文字母和运算符等。由字母表中的符号,我们可以组成基本的单词,例如数123由1、2和3这三个数字构成,而标志符abc由a、b和c这3个字母组成。在编译领域,给单词起了一个专门的术语,叫Token,其中包含了单词的类型和单词的值。例如123和456同样是数,但它们的值不一样。而数123和标志符abc是不同类型的单词。

     识别了Token之后,我们就可以按照上一节文法所规定的法则去组成合法的声明、表达式和语句等语法成份。下面我们来分析一下从http://download.csdn.net/detail/sheisc/8325793下载解压后的目录ucc\examples\sc中的代码。其中lex.h中的以下结构体描述了与Token相关的类型和值。lex是英文单词lexical的前缀。

typedef struct{

         TokenKind kind;

         Value value;

}Token;

    而lex.c中的GetToken()函数则完成了对Token的识别。图中第64至66行我们先跳过空格、制表符、回车和换行这些符号。图中的第71至77行完成了对标志符的识别,标志符以字母开头,后面紧跟若干个数字和字母。因为if, else和while等关键字keyword也符合标志符的特征,所以我们需要再判断一下识别出来的标志符是不是关键字,标志符主要用于变量名和函数名。文件tokens.txt包含了各token类型。从第78至85行,我们实现的是对数的识别,为简单起见,只完成了对十进制整数的识别。在C编译器中,我们要做得更复杂些,因为有十进制整数、十六进制整数、float和double浮点数之分。当然,还有许多简单的单词,是由一个字符构成的,例如+、-、*和/等运算符,在第87至95行主要是针对这些token的识别。这就是一个简单的用纯手工打造的词法分析器。翻开经典的龙书,我们发现词法分析这一章竟然占了大几十页的篇幅,引入了自动机和正则表达式等概念,用正则表达式来定义单词的构成规则,而用自动机来识别相关单词。实际上这些理论是为了实现词法分析器的自动构造。类似的,在文法的基础上构造语法分析器,实际上也有自动和手工之分。龙书中语法分析这一章中引入的难懂众生的LR分析实际上就是为了自动生成语法分析器。因为选择手工打造词法和语法分析器的技术路线,我们可以把自动机、正则表达式和LR分析暂且搁置。


图1.3 GetToken()

 

    下面我们以PrimaryExpression为例,讨论一下如何由产生式编写非终结符相应的函数。显然,此处对PrimaryExpression而言,有id, num和(expression)这三个候选式。如果当前从输入流中得到的token类型是TK_LPAREN,即左括号,图1-4的第57至60行则去识别产生式PrimaryExpression的候选式(expression)。我们预期接下来的字符串应该是加了一对括号的表达式。通过NEXT_TOKEN跳过左括号,读入下一个token。查看NEXT_TOKEN的宏定义,可以看到我们调用的其实就是前文所述的词法分析器GetToken()。

#define      NEXT_TOKEN        do{curToken= GetToken();}while(0)

     识别左括号之后,由该产生式所制定的规则,我们预期接下来的token会构成一个合法的Expression,而Expression是非终结符,我们也会编写一个名为Expression()的函数来识别之。此处只要调用函数Exprssion()即可,从Expression()函数返回后,我们期望当前终结符是右括号。在Expect(TK_RPAREN)中会进行比对,如果不是右括号,则报错;否则取下一个token作为当前token。而第53-56则是处理遇到标志符id和数num的情况,在第61至63行则处理其他情况,遇到的其他token都被视为非法的输入。因为任何由PrimaryExpression生成的字符串,只能以id、num或者左括号开头。换言之,PrimaryExpression的首符集是{id, num , ( }。同理,我们可以手工打造其他产生式对应的识别函数。这里,我们再次用到了递归,文法用到了递归定义,相应的识别函数自然就是递归调用。

      PrimaryExpression  -----> id  |  num  |  (Expression)  


                                   

图 1-4 PrimaryExpression()

    在图1-4中第54行,我们调用了函数CreateAstNode()创建了一个astNode结点,用于存储通过语法分析所得到的信息,例如遇到数123时,我们要记录在astNode结点中记录其类型为TK_NUM,值为123。struct astNode的定义如下所示。其中,op用于存放结点的类型,而value代表了结点的值。而kids[2]很自然地让我们联想到数据结构中的二叉树结点。对于30 * 50这样的表达式,分析到30时,我们会为30构造一个astNode结点,遇到运算符*时,也会为乘法运算符构造一个结点。显然乘法是个二元运算符,我们可用kids[2]用来分别指向乘法运算符的左、右操作数。通过语法分析,我们最终可得到由许多astNode结点通过kids[2]指针相连构成的一棵树,这棵树被称为抽象语法树,即Abstract Syntax Tree,缩写为ast。抽象语法树与图1.1中的分析树是不同的。分析树中的内部结点由非终结符构成,而叶子结点由终结符构成,整棵分析树反映的由文法开始符号进行推导的过程。我们实际上没有必要去显式地构造分析树,因为每个非终结符与语法分析器中的一个函数相对应,所以如果我们把这些函数相互调用的关系画成一棵树,则这棵树就是分析树。所以,分析树有时也被称为具体语法树,反映这棵树与产生式直接相关。而抽象语法树的内部结点通常为运算符,如前面的乘法运算符,叶子结点则是运算符所需要的操作数。此处“抽象”反映的是我们从token流中提取信息,构造适合我们进一步分析和处理的语法树的过程。

typedef struct astNode{

     TokenKind op;

     Value value;

     struct astNode * kids[2];

} * AstNodePtr;

    接下来我们就来看看如何处理分析由若干个PrimaryExpression()相乘或相除构成的乘法表达式MultiplicativeExpression。这里,我们把关注的焦点放在了如何实现运算符的左结合和右结合上。例如对于表达式100/10/2而言,如果我们规定除法运算符是左结合的,因为分析是从左到右进行的,按图1-5的第71-84行的代码,我们可以很自然地构造一棵适合进行左结合运算的抽象语法树,分析完100/10时,就可以构造一个AST子树,这棵树以除法运算符为根结点,100和10对应的结点为左右操作数,因为接下来的token仍然是除法运算符,则可当前得到的AST子树作为左操作数,用2对应的结点作为右操作数,去构造一棵新的以除法运算符为根结点的AST子树,这个过程循环进行,所以在第73行我们用的是while循环。而如果规定除法运算法是右结合的,则我们可按图1-5中的第86-100行的代码进行处理,与代码对应的产生式如下所示:

         MultiplicativeExpression  ----->  PrimaryExpression

         MultiplicativeExpression  ----->  PrimaryExpression * MultiplicativeExpression

         MultiplicativeExpression  ----->  PrimaryExpression / MultiplicativeExpression

 

      这实际是右递归的产生式。对100/10/2而言,仍然是从左到右进行扫描的,但分析完100/10后,我们还不能为之构造AST,我们要看一下10后面是否还有乘法或除法运算符,如果有,则10要与其右侧的除法运算符先结合。所以先构造出来的AST子树是10/2,之后再以这棵子树为右操作数,之前已经分析得到的100作为左操作数,以除法运算法为根结点,构造一棵适合进行右结合运算的AST。对候选式PrimaryExpression /MultiplicativeExpression的分析过程与上述非终结符PrimaryExpression的分析过程类似,对于候选式中的非终结符,我们直接调用与之对应的同名函数来识别接下来的token串;对于候选式中的终结符,我们判断一下当前读入的token是否与之一致。图1-5中的第87、94和96行实际上就完成了这个分析过程,其余的几行代码实际上用于创建AST结点。

        

                                               图1-5       MultiplicativeExpression()

    对100/10/2,如果除法是右结合的,则运算结果为20;而如果除法是左结合的,则运算结果是5。如果定义一个新语言,硬要把除法规定为右结合,这是违反直觉和习惯的,是件无意义的事情。此处只是以此为例来说明我们应如何处理右结合的运算符。在C语言中,a = b =c是右结合的。在图1-5中第77行和第92行,我们还看到了一个名为NewTemp()的函数。以a*b*c为例,如果乘法是左结合的,我们先进行的运算是a*b,我们需要把运算结果存到一个临时变量中,之后再用这个临时变量与c进行乘法,结果再存到一个新的临时变量中,如下所示

         t1 = a * b

         t2 = t1 * c

      处理完乘除法运算,我们再来看一下加减法运算的处理函数AdditiveExpression(),如下图1.6所示,可以发现它与MultiplicativeExpression()的大部分代码都是相似的,。因为它们都是二元运算符,只是运算符不同而已。我们知道,在C语言中,还有许多类似的二元运算符,如果为每个这样的非终结符构造一个独立的处理函数,则会出现大量的代码冗余。我们可以考虑把对二元运算符的处理归并到一个函数中统一处理。在后续章节分析UCC编译器的源代码时,我们会在ucc\ucl\expr.c中看到static AstExpression ParseBinaryExpression(int prec)这个函数,C语言中所有的二元运算表达式都由这个处理函数进行分析。

                    


                                            图1.6  AdditiveExpression()

    至此,我们已基本完成对加减乘除四则运算的分析,下图1.7给出了分析完(a+b)*c后,分析器为我们构造的语法树。结合expr.c中的代码,我们还可以画出相应的分析树,分析树实际上反映了函数的调用关系,在分析树上我们能清楚地看到语法树各结点的构造顺序。




图1.7 分析树和语法树

    后续的语义分析和中间代码生成等动作,我们都会基于形如这样的语法树上进行。下面我们再来看一下图1.8中的函数voidVisitArithmeticNode(AstNodePtr pNode)。其中的代码一定让我们条件反射般地想起了当年在《数据结构》课中那熟悉的二叉树前序、中序和后序遍历。这里,我们做的是对图1.7进行后序遍历,我们会得到以下结果。实际上,VisitArithmeticNode()就是一个简单的中间代码生成器。

      t0 = a + b

      t1 = t0 * c



图1.8      VisitArithmeticNode()




  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值