1 目的
语法分析是根据源语言的语法规则从源程序记号序列(词法分析阶段的输出)中识别出各种语法成分,同时进行语法检查,为语义分析和代码生成做准备。
2 方法
对记号序列自左向右扫描,每次读一个记号。文法推导是一棵分析树,如果匹配成功,终结符是叶子结点连起来的输入串。
2.1 自顶向下分析:面向目标分析法 就是用文法硬凑与输入符号匹配的句子
2.1.1 递归下降预测分析 (一种不确定的,带回溯的预测分析方法)
一种没什么卵用的高代价穷举试探方法,然鹅却和我们的脑回路最接近
下面,试图分析输入串abbcde是否是以下文法的句子。
概括一下就是从起始符S开始,面对非终结符要展开的时候,尽可能地凑当前指针所指的输入串字符。这种只顾眼前利益不顾子孙后代幸福的方法,后面一定会付出惨痛代价。显然,很大可能后面会回溯,代价太大。
2.1.2 递归调用预测分析(确定的无回溯分析)
1 文法要求
当然想确定每次展式,文法必须无左递归且提取左公因子后无二义性。这些要求说白了就是想让非终结符为了最左边(此时的指针位置)与输入串匹配成功,选择的推导式是唯一的。严谨一点说,就是文法中的任意非终结符A,定义A的某候选式的开头终结符号集如下:
为了不产生提取左公因子后的二义性,要求对于A任意的候选式和
,都有:
注意:若候选式可以经过有限次推导推出ε,则其FIRST集含ε。
注:对形式语言不太熟悉的小伙伴,这里回顾一下消左递归的方法(想得美,我才懒得写呢 只写核心的一个变化步骤:
2 状态转换图
1)构造
文法的每个非终结符产生式对应一个自动机,由初始状态S出发,寻找状态转移过程,分以下三种情况:
第一种容易理解,第二种本质上是中间一段的序列被A终结符推导匹配,然后回到T。第三种表示如果S无法匹配当前a且有空转移,则到T状态进行匹配。
2)化简
将输入自身到达的状态可以和初始状态进行合并,不同状态在输入相同到达状态相同时,也可进行合并,还有空转移也需注意。下面举个栗子:
3 利用状态转移图写出分析程序
这种与其啰里八嗦写一堆,当然不如看个栗子自己顿悟。先上化简完的状态图 :
则E的过程:
void procE(void){ procT(); if(char == '+'){ forward pointer; procE(); } }
T的过程:
void procT(void){ procF(); if(char == '*'){ forward pointer; procT(); } }
是不是很简单呀,这里自行尝试写F的程序吧。没有答案(我是内种写完两个简单的就跑路的小编么?咳咳,这是F,自行对照:
void procF(void){ if(char == '('){ forward pointer; procE(); if(char == ')') forward pointer; else error(); } else if(char == 'id') forward pointer; else error(); }
请自行思考一下,为什么只有F的代码里有错误处理error().别看了,这回真是让自行思考,hhh
2.1.3 非递归预测分析(确定,无递归,无回溯)
1 非递归模型
还记得算法里面,把一个递归程序改成非递归程序,思维上是件不太容易的事情,非递归程序在问题理解上也没有递归程序清晰明了,但是非递归程序开销更小,效率更高。探索非递归来代替递归是一件可以,且有必要的事。话不多说,先上非递归模型:
em,一个看起来像是图灵机,又不知所谓的图
反正看起来核心的预测程序,要从缓存区去输入字符,查找分析表M之后,借助栈进行操作最后得到输出。(这不废话么 缓冲区没什么好说的,分析栈栈底是$,里面还可以放文法的任意终结符和非终结符。M是二维表[A,a],一个坐标是非终结符,另一坐标是终结符或者$,表项内容是一个动作。输出其实是刚刚用到的推导式。假设当前栈顶为X,输入为a,则有以下四种情况以及相应处理过程。
(我是真懒,懒得打,hhh)下面更懒 再贴个栗子:有如下预测分析表
分析id + id * id的过程:
灰常长,自己跳着随便看看,看跟自个儿想的一不一样就行,毕竟过程还是非常easy的嘛。所以现在的关键问题就变成了,这张表是怎么变出来的。
2 构造M分析表
1)构造FIRST集合
还记得FIRST么,这里沿用定义,只不过把FIRST的作用范围从A的候选式扩展为任意文法符号串。那怎么写出FIRST的全集呢?
(贴图警告
好长一段,反正就是除了终结符自个儿也是自个儿first,以及定义中的基本情况,推导式终结符前缀是first以外,如果推导式前缀是个非终结,再去找那个非终结的first,考量的当前前缀有ε的话,在找下一个非终结的first。如果全有ε,再把ε加进自个儿的first。其实挺容易想明白的。还在纠结的话,看栗子。(我真是个花里胡哨的沙雕
文法: FIRST表
2)构造FOLLOW集合 (什么?!first后面居然不是second?)
FOllOW呢,就是说该文法中所有句型中(划重点),紧跟在A之后出现的终结符号或者$组成的集合。
怎么构造呢?(贴图没差
这次的主要思路是在所有产生式的右边找自个儿,找到之后看自己后边都有啥。后面是终结符就加进去,不是就想办法蹭非终结符的first。如果后面有ε,还能碰瓷产生式左边那个非终结符的follow。所以显然follow里面没有ε。让我们看一下first栗子里的那个文法的follow表:
已经懒到中间内行first都懒得删了
3)构造分析表M
栗子大法好,实践出真知。我们把刚刚first和follow的栗子在这里再用一下,得到M:
细心的朋友们这里会发现,咦,纳尼?怎么[S', e]有俩推导式呢,那程序看到这个不是就懵逼了么?对呀,这谁的锅呢,既然是标答,那肯定不是我们造表的锅啊,对,没错,是文法的锅(题给的文法是二义性文法)。一个好的文法造出来的表不应该让预测分析程序懵逼,我们给这种好的文法(无二义性文法)起名叫做LL(1)文法。(这名字是不是起得及其诡异?L(从左到右扫描输入串)L(生成输入符号串的一个最左推导)1(程序每步动作,向前看一个符号)就这么来的呗
那这玩意儿有没有啥实际点儿的判断方法呢?首先不能左递归,然后:
2.2 自底向上分析(太长了太长了,下一节专门讲)
3 语法错误恢复策略(出现错误时,为了使分析继续进行)
3.1 紧急恢复:分析程序每次抛弃一个输入记号,直到向前指针所指向的记号属于某个指定的同步记号集合(适当选取,一般包括结束符分号,end等)
3.2 短语级恢复:出错后对剩余输入做局部纠正,替换剩余输入串的前缀。例如分号代替逗号,删除多余分号,插入遗漏的分号等。(分号大法好)
3.3 出错产生式:增加产生错误结构的产生式,扩充源语言的文法。
3.4 全局纠正:做尽可能少的修改,使得输入串语法正确。处于理论阶段。就说真要有这么完美的,为啥前面还啰嗦一堆
目前为止,是不是3.1简单一点,靠谱一点?我们以3.1为例,描述一种非递归预测分析的错误处理方案。3.1的关键在于选取(构造)同步记号。还记得非递归预测分析什么情况可以判断语法错误么?
针对(1):将栈顶的终结符号弹出即可
针对(2):向前移指针,移到此时的栈顶非终结符和剩余输入符号串可以重新协同工作(就是M表里他俩不为空了)。实现方法是改造M,如果非终结符A的follow里有b,然后M里的[A,b]还是空,就在这个位置添加同步记号synch。然后如果遇到情形(2),就前移指针,若[A,a]是synch,就把A从栈顶弹出。是不是还有些细节懵懵的?栗子栗子栗子!
凑活看吧,我累了,呜呜呜呜
注:本文章所有知识内容和样例均来源于编译技术与原理 (李文生) 第2版