语法分析——自上而下

  衔接上一章的词法分析,这章就到了语法分析。语法分析大体上分了两种:自上而下和自下而上。这章的主要内容是前者,下一章会对后者进行介绍。其实学到了这里应该清楚语法分析和词法分析的关系了。在实际编译的过程中并不是先进行一遍词法分析,再来一遍语法分析。两者其实是并行的。从缓冲区中读入一定字符串,经过词法分析合格后进行语法的审核。符合语法规则通过,不符合就报错。这样周而复始直至结束。

  语法分析要联系到第二章的内容了,还记得什么是文法吗?就是那个使用四元式表示(不过四元式在本章中没什么地位),表示语法规则的文法啊。其实我觉得这章的一个核心内容就是告诉如何利用文法来进行语法分析的,更为通俗的一种说法是文法如何找到语法错误的呢?且听我慢慢道来!

  先列举一下本章的关键词:自上而下、FIRST集、FOLLOW集、LL(1)分析法,递归下降分析程序、预测分析程序、LL(1)文法的出错处理。

  希望通过这篇文章能说明这些关键词是什么,更希望说明为什么。下面正式开始吧!

语法分析——自上而下

  这章的题目就是这个,其实说白了语法分析就是个匹配字符串的环节嘛。看你输入的字符串在我已表示的语言中能否找到对应的。之前第二章介绍过对于任何语法都可以构建成一颗语法分析树。从开始符号开始,一个又一个的产生式衔接,匹配结束且刚好到达叶子节点。整个过程就是自上而下的语法分析。Hhh,说起来很轻松哈,那如何在那么多个分岔口找到最正确的一条呢?这就需要学一下正确的语法分析步骤喽~

  自上而下也是一个推导的过程,之前提过推导分为最左推导和最右推导。在自上而下中,统一规定使用最左推导

  先来看一个简单的例子(这个例子没有说明语句是否符合语法规则,只是表示一下推导过程):

在这里插入图片描述

  在本例中每步都要进行一个产生式的选择,选择的原则就是尽可能让终结符贴近要表达的。从字符逐个匹配的角度看,本例中的字符串一个个进行匹配并且恰好都是正确(为什么这么说呢?来看下面这个:)

在这里插入图片描述
  对于上图中,一眼就可以看见第二才是正确的,但是在计算机识别过程中由于**中的第一个和xy中达成了匹配,是不会立即检验下一个字符的。当检验下一个字符时原式中是y而自己拼的是*,计算机这才知道:哦,错了!其实错了也没关系嘛,可以采用回溯的思想退回到上一个成功匹配的字符处选择另一个产生式,就选到了x*y了。回溯当然OK啦!可我们都知道回溯是要多耗费一些资源(时间/空间)的,避免回溯是学计算机人的伟大使命。但似乎随着算力和空间的发展,当我们真的不在乎回溯的影响时,就可以肆意妄为了吗?答案是No!来看下面这个例子:

在这里插入图片描述

  按照前面的思想:不停的回溯重来,结果就会使Saaaaaa…aaaa,这样运行下去再大的空间也终有一日会满(积硅步,行千里)。这种情况就叫做左递归。左递归作为语法分析最大的敌人之一,自然需要被先解决。那左递归如何可以被消除呢?

消除直接左递归的方法

在这里插入图片描述
  再配上我独门的记忆口诀:引入新的非终结符,把所有原来没有左递归的项加上新的非终结符组成一个产生式,再为新非终结符构造一个产生式:把之前左递归的右边系数加上新非终结符构成一个右递归,最后再加上个空字即可大功告成。

  举个例子:

在这里插入图片描述
  注意上面的副标题我多加了“直接”两字,只有一眼直接看到S->Sx的才是左递归吗?当然没有这么简单~看下例:

在这里插入图片描述
  如果把三个产生式一层层消除,就按照SQR的顺序依次带入前者(即Q带入S,R带入Q)最后会得到这样的结果:

在这里插入图片描述
  当出现这样的情况时,就发现上面的解决办法只能解决直接左递归。对于这种间接的还是比较麻烦。下面介绍一下通用的消除左递归办法:

在这里插入图片描述
  其实这个说是办法,方法还是属于暴力迭代。尽可能消去无用的非终结符达到一眼看出直接左递归的情况为止,再按照上面的方法解决问题。

  这里有个事情需要强调下:第一步对于顺序的排列是无所谓的。选择不同的顺序可能会导致最后构造出的文法不完全相同(因为在选择添加新的非终结符时,非左递归项可能不同),但能完成同样消除左递归的功能即可。

  所以对于上面问题的解决办法就是:

在这里插入图片描述
  说到现在,左递归就基本说完了。其实回想整个过程,左递归是语法分析必须要避免的。具有消除左递归的方法,但同样也需要付出:引入新的非终结符。(正所谓:天上不会掉馅饼~)

  上文介绍到,我们要避免回溯。那想想为什么会出现回溯呢?不就是因为一个“虚假”的匹配嘛?如果我们能够避免这种虚假,不就可以消除回溯吗?什么意思呢?比如A->x1|x2|x3…,下个字符要匹配a,如果x1,x2,x3等所有项中只有一个第一个字符是a,那不就直接选这个,对了就继续走,不对就匹配失败呗。这就是提取公共左因子

提取公共左因子

在这里插入图片描述

  其实这个方法有点像小时候学加法的时候提取公因式,当把公因式提取出来时对于当前字符只有了唯一项,就不会出现当前字符造成回溯的情况了。需要注意的是:一定要提取最长公共前缀

FIRST集、FOLLOW集

  按照思路:先懂是什么,再懂为什么?先对如何计算两种集合做一个介绍。

FIRST集

  FIRST集的全名叫做终结首符集,每一个非终结符都有。就是由该非终结符能够推导出的第一个终结符或空字组成的集合。看一下标准的概念:

在这里插入图片描述
  那求某一非终结符FIRST集的步骤呢?看下面(先形式化的定义下,后面有配合的例题进行讲解):

在这里插入图片描述

FOLLOW集

  FOLLOW集是推导过程中非终结符紧接着的终结符组成的集合。

在这里插入图片描述
  对于FOLLOW集有两个需要特别注意:

    1)对于起始符的FOLLOW集需要引入“#”符号

    2) 在FOLLOW集中不可能出现空字ε

  求FOLLOW集的步骤如下:

在这里插入图片描述
  从上面可知,FOLLOW集只关注于产生式的右边。对于某一非终结符,影响其FOLLOW集的只是右边包含该非终结符的产生式。

  如果你从上面文字性的就看懂了,求解FIRST集和FOLLOW集的步骤。那你真是个天才,像我这种稍微有点儿笨的只能看看例题理解理解哈:

在这里插入图片描述
  这类题顺序是先求解FIRST集再求解FOLLOW集。

  对于FIRST(E),E的产生式第一项是T是非终结符,所以求E必须先求出T的FIRST集,FIRST(T)中的内容都属于FIRST(E)。转而求解FIRST(T),T的产生式第一项是F,转而求解F。F的第一项有两个并且均为终结符——{(,i};所以可反推出FIRST(E/T/F)均为{(,i};求解FIRST(E’),E‘有两项组成第一个是终结符+,加入集合。第二个是空字,按照定义也加入集合。最终得到{+,ε}。同理可得FIRST(T’)为{*,i};

   对于FOLLOW集,首先E作为开始符,先将#加入FOLLOW(E)中,接着寻找右侧产生式带E的,看后面是否为终结符。本例中E后只有),将其加入FOLLOW(E)中。对于E’,在前两个产生式中出现,但后面无任何符号(相当于空字ε),于是顺承左边非终结符的FOLLOW集,即FOLLOW(E)。并且FIRST(E)中无空字ε。FOLLOW(E’)= FOLLOW(E) = { ),#}。接着计算FOLLOW(T),T后为E’,于是将FOLLOW(E’)及非字FIRST(E‘)加入T中,得出FOLLOW(T)={+,),#}。对于T’后无字符,顺承T,且FIRST(T)中无空字,FOLLOW(T‘)=FOLLOW(T’)。F后为T‘,FIRST(T’)含有空字,将FIRST集非空字符及FOLLOW(T‘)加入FOLLOW(F)中={*,+,),#}。

  弄懂以上的两个过程,证明已经会求解FIRST集和FOLLOW集了,也许可以参加考试了。但是为什么需要FIRST集和FOLLOW集呢?仔细想想FIRST集和FOLLOW集究竟干了什么呢?我将其理解为开始匹配和继续匹配。当我们面临一个新的非终结符时,可能由于产生式的多个或,选错了就要进行回溯。务必一击就中,就可以通过FIRST集,FIRST集中每一项都是可直接到达,且路线唯一。而FOLLOW集就是当前非终结符下一个终结符可以是哪个?如果当前的符号不在备选范围内,就说明不存在下一个到达提供字符的可能,即认定失败。成功继续,失败结束。两个集合起的作用就是让一切看起来尽量直观。

LL(1)文法

   LL(1)文法是一类特殊的文法,其需满足的条件如下:

在这里插入图片描述

  其实LL(1)文法就是进行语法分析的一整套流程,满足该文法字符串有效。不满足就无效。之前一直在纠结于:LL(1)文法的作用是什么呢?其实LL(1)也是一种文法,文法就是用来定义语言的。不过LL1(1)文法特殊在有很多特别的限制。

递归下降分析程序

  其实递归下降程序就是文法(该文法需要确保已无左递归以及公共因子的情况,即LL(1)文法)的分析过程。把每一个产生式写成了一个程序。所以对于任何一个非终结符都有一个属于子集的程序。我把这个理解成贴近于高级语言的语法分析检测过程。文字性的定义放一张图:

在这里插入图片描述
  举一个例子,再加上我总结的一点儿规律吧:

在这里插入图片描述
  程序开头:PROCEDURE xxx(非终结符名称)

  对于直接由非终结符组成,如E和T。BEGIN和END中间夹着产生式右端所有非终结符,以分号连接。对于第一项为终结符,如果是匹配的就调用ADVANCE。SYM代表当前获取到的单词(即输入字符)。如果是非终结符就调用相应的过程。空字需要特殊处理(虽然在上面的图里没有展示出来),什么时候可以使用空字去进行匹配呢?当需要空字说明目前的SYM与非终结符不匹配。SYM可能是当前非终结符的FOLLOW集,那么对当前非终结符使用空字匹配就可以保证对SYM的匹配。但这有一个要求:SYM必须在取空字非终结符的FOLLOW集中使才可以。否则就是一种错误。说的有点复杂:简单点就是要想成功的取到空字,当前的输入字符必须在非终结符的FOLLOW集中。

扩充的巴科斯范式

  扩充的巴科斯范式是对文法符号的扩展,具体扩展包括三种:

在这里插入图片描述
  对以上做个小结:

  递归下降程序就是自顶向下的语法分析,扩充的巴科斯范式是表示文法的一种方法,并且该方法能够比较容易的处理左递归问题。

预测分析程序

   到了预测分析程序就有点偏实用了,采用预测表和栈结合的方法。对一个字符串进行判断。

  预测分析表的作用就是能够直观看出当前非终结符在指定输入下选择哪个具体的产生式。表的列项是所有的非终结符,横项是所有的终结符。具体的坐标要么是产生式要么为空(说明不存在)。形式化的定义如下:

在这里插入图片描述

  还拿刚才的例子,说一下预测分析表怎么构造:

在这里插入图片描述
  想求预测分析表还是要先求出FIRST和FOLLOW集。按顺序说,写每一个的时候先看FIRST集,对非终结符求每一个候选式的FIRST集合。结果大体可分为两类:有空字和无空字。对于无空字像E和T,在FIRST集合对应的单元格填入该候选式。比如i输入E的FIRST集。在(E,i)中填入候选式E->TE’。如果FIRST集合含有空字,说明xxx->ε。则在该非终结符的FOLLOW集对应的单元格放入该产生式。比如E‘的FIRST集中包含空字,并且FOLLOW(E’)={},#}。于是在<E’,)>、<E’,#>中加入产生式E’->ε

   需要注意两个事情:

    1. 空出来的地方就是不合法的情况

    2. 表内每个产生式右端只可能出现一种情况

  说完了表,再说说栈。都知道栈是一种数据结构——后进先出。还记得前面说要在起始符的FOLLOW集中加入#嘛?这就是为了在此匹配。在输入字符串的最后加入#,如果两个#匹配到,说明分析结束。结束不代表正确。下文关于错误处理会给出解释。对于栈,在此不在多说(其实是我也不会,不过上机的时候应该需要。如果过后搞懂了,再来补充)。附上形式化描述:

在这里插入图片描述

LL(1)文法中的出错处理:

  文法中的错误难免出现,但如果一出错就全部停止。未免有些影响效率。我们崇尚的是那种报错了保留错误位置并且程序能够继续执行的情况。如果在预测分析处理的过程中发现了错误,错误分两种:

  错误一:栈顶的终结符与当前的输入符号不匹配

  错误二:非终结符A处于栈顶,面临的输入符号为a,但预测分析表中M的M[A,a]为空。

  解决方法采用忽略的思想,不断跳过直至下一次匹配再此出现。(此处解释了分析结束并不代表语法正确)

在这里插入图片描述

  说到这里自上而下的语法分析主要内容就基本结束了,整个过程说起来比较简单。但如果真正实现起来想必是困难重重。收拾心态,准备迎接更大的挑战,继续加油叭~

感谢编译原理课程谢老师对本文的耐心修改,同时感谢各位博主的优秀文章作为参考。

因作者水平有限,如有错误之处,请在下方评论区指出,谢谢!

猜你喜欢

转载自blog.csdn.net/gls_nuaa/article/details/108878372