编译原理学习笔记--语法分析(一)


1 目的

  语法分析是根据源语言的语法规则从源程序记号序列(词法分析阶段的输出)中识别出各种语法成分,同时进行语法检查,为语义分析和代码生成做准备。

方法

  对记号序列自左向右扫描,每次读一个记号。文法推导是一棵分析树,如果匹配成功,终结符是叶子结点连起来的输入串。

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版

 


免责声明!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系本站邮箱yoyou2525@163.com删除。



 
粤ICP备18138465号  © 2018-2025 CODEPRJ.COM