• 整理编译原理笔记
  • 编译原理和形式语言自动机关系很大,可以结合自动机的内容一起看
  • 教材《程序设计语言 编译原理(第3版)》—— 陈火旺 等
  • 个人理解,仅供参考,错误欢迎指出

文章目录

  • ​​〇、思维导图​​
  • ​​一、语法分析器的功能​​
  • ​​二、自上而下分析面临的问题​​
  • ​​(1)自上而下分析的基本方法​​
  • ​​(2)带回溯自上而下分析的问题​​
  • ​​三、LL(1) 分析法​​
  • ​​(1)左递归的消除​​
  • ​​(2)消除回溯、提左因子​​
  • ​​1. FIRST(X) 终结首符集​​
  • ​​2. 提取公共左因子​​
  • ​​(3) LL(1)分析条件​​
  • ​​1. FOLLOW集​​
  • ​​2. LL(1)文法(不带回溯的自上而下分析文法)的定义和判据​​
  • ​​3. 用LL(1)文法构造最左推导的过程​​
  • ​​四、预测分析程序​​
  • ​​(1)预测分析表的概念和构成​​
  • ​​(2)预测程序的工作过程​​

〇、思维导图

因为这里涉及到太多定义、概念、方法,列出思维导图以理清思路。以下是一个大体的框架,最后全部展开的话这个图太大了看不清,可以在​​这里​​下载完整图,用MindMaster软件打开

编译原理(1)—— 语法分析(自上而下)_语法分析

一、语法分析器的功能

  • 在词法分析识别出单词符号的基础上,分析判定程序的语法结构是否符合规则
  • 高级语言的语法适合用上下文无关语言描述,它的描述能力比正则文法、正则语言更强
  • 本质上,语法分析器的作用,就是按上下文无关文法的产生式,识别输入串是不是一个句子。这里的输入串是由单词符号(文法的终结符)组成的有限序列
  • 语法分析可以粗略地分成两类:自上而下分析法自下而上分析法

二、自上而下分析面临的问题

(1)自上而下分析的基本方法

  1. 自上而下分析本质上是一种试探过程,是反复使用不同产生式谋求匹配输入串的过程
  • 从推导的角度看,从文法开始符号出发,试图推导出与输入符号串相同的符号串。一般来讲,构造出的推导是最左推导
  • 从语法树的角度看,从根节点,试图向下构造一颗语法树,其端末节点从左到右连接起来的串正好与输入符号串相同
  1. 例子:假定有文法: 编译原理(1)—— 语法分析(自上而下)_递归_02 ,有输入串编译原理(1)—— 语法分析(自上而下)_语法树_03,用上而下法进行语法分析
  1. 构造输入串 x*y的语法树:
    第一:S ⇒xAy ⇒ x**y… (用A→**试探,匹配失败发生回溯)
    第二:S ⇒ xAy ⇒ x*y (用A→*试探,匹配成功,编译原理(1)—— 语法分析(自上而下)_语法树_04是)
  2. 在使用非终结符A的产生式时存在两个候选式,首先用A→**进行试探不能获得匹配,然后使用A→*进行试探,获得匹配。很明显这是一种带回朔的反复试探过程。这种方法使得系统实现时的效率存在极大的问题,甚至不能进行
  3. 用语法树表示

(2)带回溯自上而下分析的问题

  1. 无法处理带有左递归的文法。例如产生式编译原理(1)—— 语法分析(自上而下)_语法分析_05,在发生试探时,会不断地把最左的P替换为编译原理(1)—— 语法分析(自上而下)_编译原理_06,陷入无限循环。这只是一个简单的直接左递归示例,对于间接左递归也会有一样的问题
  2. 编译原理(1)—— 语法分析(自上而下)_自上而下_07


  3. 回溯会导致算法复杂度很高
  4. 虚假的成功匹配:判断匹配时,实际是用两个指针遍历输入串和试探串进行比较。还是看上面的示例,当用用A→**试探时,比较第一个*时还是匹配成功的,直到比较y和第二个*才能确认匹配失败。这种虚假匹配会导致回溯算法更加复杂
  5. 当最终分析不成功时,难以知道输入串中出错的确切位置
  6. 时间复杂度太高,这实际上是一种穷尽一切组合的试探法,效率极低
三、LL(1) 分析法

这一节里我们将探讨消除左递归和克服回溯的方法,寻找自上而下分析的有效算法

(1)左递归的消除

  1. 消除直接左递归

    1. 方法:
      编译原理(1)—— 语法分析(自上而下)_语法树_08编译原理(1)—— 语法分析(自上而下)_自上而下_09编译原理(1)—— 语法分析(自上而下)_语法分析_10编译原理(1)—— 语法分析(自上而下)_编译原理_11编译原理(1)—— 语法分析(自上而下)_编译原理_12编译原理(1)—— 语法分析(自上而下)_自上而下_13

    2. 例:
      编译原理(1)—— 语法分析(自上而下)_编译原理_14编译原理(1)—— 语法分析(自上而下)_递归_15

  2. 消除一切左递归

    1. 前提:

      1. 不含回路(编译原理(1)—— 语法分析(自上而下)_自上而下_16
      2. 不含以编译原理(1)—— 语法分析(自上而下)_递归_17为右部的产生式
    2. 方法:

      1. 将文法的所有非终结符按任意顺序排列成编译原理(1)—— 语法分析(自上而下)_编译原理_18

      2. 按1中顺序执行以下操作
      3. 编译原理(1)—— 语法分析(自上而下)_语法树_19

      4. 这一步相当于把产生式互相带入,把间接左递归转为直接左递归,这样就可以用消除直接左递归的方法处理
      5. 化简所得到的文法,删除从开始符号出发永远达不到的非终结符的产生式规则。

    3. 例:非终结符的排列顺序会影响结果形式,但各结果之间是等价的
    4. 编译原理(1)—— 语法分析(自上而下)_自上而下_20


    5. 编译原理(1)—— 语法分析(自上而下)_递归_21

(2)消除回溯、提左因子

  • 要构造堪用的自上而下分析器,就必须要提高效率,这意味着我们要找到消除回溯的方法。
  • 回溯会发生的根本原因在于匹配时可能有多个可选择的产生式,想消除回溯,就意味着:分析器对文法的任何非终结符。当它要匹配输入串时,能够根据它所面临的输入符号准确地指派它的一个候选去执行任务,并且此候选的匹配结果应该是确定无疑的(要么成功要么失败,不会出现前面的“虚假成功”现象)。这样一来,每一次匹配都有唯一确定的产生式选择,自然不会发生回溯了,而每一个选择匹配成功与否,都决定了输入串能否被文法接受
  • 在消除回溯前,首先要确保文法不含左递归

1. FIRST(X) 终结首符集

  1. ​终结首符集FIRST(X)​​:X是所有可能推导开头终结符可能的ε
    1. 定义:
      有文法编译原理(1)—— 语法分析(自上而下)_语法分析_22
      特别是,编译原理(1)—— 语法分析(自上而下)_递归_23,则编译原理(1)—— 语法分析(自上而下)_语法树_24
    2. 构造方法
    3. 编译原理(1)—— 语法分析(自上而下)_自上而下_25


    4. 示例:
    5. 编译原理(1)—— 语法分析(自上而下)_递归_26


    6. 对文法的符号串α构造FIRST集:
    7. 编译原理(1)—— 语法分析(自上而下)_语法树_27


    8. 分析
      1. 回顾一下最开始 编译原理(1)—— 语法分析(自上而下)_语法树_28 的例子,但输入串检测到终结符​​*​​时,由于非终结符A的两个候选​​**和*​​有​​FIRST(**) ∩ FIRST(*) = {*}​​,这样A在面对输入符号​​*​​时,就无法唯一地指派一个候选。
      2. 简单说:非终结符A在匹配输入串时,当它面对输入符​​*​​时,发现以自己为左部的产生式中,有多个右部都能在若干推导后得到以​​*​​开头的一个符号串,这样A就不能确定这里该用哪个产生式匹配了。
      3. 结论:如果 “非终结符A的所有候选的终结首符集” 两两不相交,即A的任意两个候选αi和αj,都有FIRST(αi) ∩ FIRST(αj) = ∅,那么当要求A匹配输入串时,A就能够根据它面临的第一个输入符号a,准确地指派某一个候选式去执行任务。这个候选式就是那个终结首符集含a的α

2. 提取公共左因子

  1. ​提取公共左因子​​:用这种方法把一个文法改造为 “任何非终结符的候选的终结首符集” 不相交
    1. 方法:重复提取最大公共左因子,付出的代价是引入大量非终结符和ε-产生式
    2. 编译原理(1)—— 语法分析(自上而下)_编译原理_29


    3. 例:
    4. 编译原理(1)—— 语法分析(自上而下)_自上而下_30


(3) LL(1)分析条件

  • 如果一个文法不含左递归,且满足每个非终结符的所有候选首符集两两不交,是否就一定可以进行有效的自上而下分析呢?看如下示例
    1. 题目:
    2. 编译原理(1)—— 语法分析(自上而下)_语法树_31

    3. 先求FIRST()
    4. 编译原理(1)—— 语法分析(自上而下)_编译原理_32

    5. 开始自上而下分析过程,由于起始符号E只有一个候选​​TE'​​,且第一个输入符i满足​​i∈FIRST(TE')​​,故用​​E→TE'​​推导
    6. 编译原理(1)—— 语法分析(自上而下)_自上而下_33

    7. 我们要构造一个最左推导,接下来从T出发匹配输入串。此时面临的输入符还是i。T只有一个候选​​FT'​​,且有​​i∈FIRST(FT')​​,故用​​T→FT'​​推导
    8. 编译原理(1)—— 语法分析(自上而下)_递归_34

    9. 接下来从F出发,此时输入符还是i,它的候选中只有i满足​​i∈FIRST(i)​​,因此用​​F→i​​推导。这使得第一个输入符i得到匹配,IP指针后移
    10. 编译原理(1)—— 语法分析(自上而下)_语法分析_35

    11. 现在要从T’出发匹配+。但是这里T’所有的候选首符集都不包含i,但有​​T'→ε​​,所以我们不妨让T’​​自动得到匹配​​(即匹配于空字ε,输入符号不读进
    12. 编译原理(1)—— 语法分析(自上而下)_语法分析_36

      • 何时可以自动匹配:当非终结符A遇到输入符号a

        1. a∉A的任何候选首符集
        2. A的某个候选首符集包含ε
        3. a是允许在文法的某个句型中跟在A后面的终结符

        如果不满足以上3条,a在这里出现就是一种语法错误

    13. 省略若干推导,最后得到完整的最左推导语法树
    14. 编译原理(1)—— 语法分析(自上而下)_编译原理_37

1. FOLLOW集

从上面的例子可以看到自动匹配的条件难以判断,为了解决这个问题,引入FOLLOW集

  1. 简介:

    1. ​FOLLOW集​​:有文法G = ( V, T, P, S ),对G的任何非终结符A,我们定义:
      1. 编译原理(1)—— 语法分析(自上而下)_语法分析_38
      2. 特别地,若编译原理(1)—— 语法分析(自上而下)_递归_39,则规定编译原理(1)—— 语法分析(自上而下)_递归_40
    2. 换句话说:FOLLOW(A)是所有句型中出现在紧接A之后的终结符或#符号
    3. 重新定义自动匹配规则:当非终结符A遇到输入符号a
      1. a∉A的任何候选首符集
      2. A的某个候选首符集包含ε
      3. a∈FOLLOW(A)
  2. 构造方法:
    重复使用以下规则,直到FOLLOW集不再变大为止

    1. 对于文法的开始符号S,置#于FOLLOW(S)中
    2. 若A→αBβ是一个产生式,把FIRST(β)(不含{ε})加入FOLLOW(B)
    3. 若A→αВ是一个产生式,或A→αВβ是一个产生式,且β⇒ε,(即ε∈FIRST(β)),则把FOLLOW(A)加至FOLLOW(B)中。(说明:因为在推导过程中如果出现…A…,现如果采用A→αВ进行推导,则句型变成…αВ…,所以有上述结论)
  3. 注意:

    1. FOLLOW集是产生式右边起作用,也就是说,对于某非终结符的FOLLOW集,影响它的只是产生式右边包含该非终结符的产生式
    2. FOLLOW集中肯定不会出现ε

2. LL(1)文法(不带回溯的自上而下分析文法)的定义和判据

  1. 文法不含左递归
  2. 对于文法中每一个非终结符A的各个产生式的候选首符集两两不相交。即,若A→α1|α2|…|αn,则FIRST(αi) ∩ FIRST(αj) = ∅ ,ε除外
  3. 对文法中的每个非终结符A,若它存在某个候选首符集包含ε,则FIRST(A) ∩ FOLLOW(A) = ∅​(解释:设终结符a∈FIRST(A) ∩ FOLLOW(A);a∈FIRST(A)推出a∈A的某个候选首符集;a∈FOLLOW(A)推出a∉A的任何候选首符集,矛盾)
  • 满足以上三条的文法即为LL(1)文法

3. 用LL(1)文法构造最左推导的过程

  1. 对于一个LL(1)文法,假设要用非终结符A进行匹配输入符号a,A的所有产生式为A→α1|α2|…|αn

    1. 如果a ∈FIRST(αi),则指派αi去执行匹配任务
    2. 若a不属于任何一个候选首符集,则
      1. 若ε属于某个FIRST( αi ),且a∈FOLLOW(A),则让A与ε自动匹配
      2. 否则,a的出现是一种语法错误

    对于一个LL(1)文法,这里的每一步都是确信无疑的。

    编译原理(1)—— 语法分析(自上而下)_递归_41

四、预测分析程序

了解自上而下语法分析的过程后,我们现在要对它找出一种形式化的描述方法,以便我们编程实现语法分析的过程。

  • ​预测分析程序​​:利用一张分析表和一个进行联合控制来实现LL(1)文法的分析,预测分析是一种非递归方法。
  • ​预测分析​​:自顶向下分析是从文法的开始符号出发,试构造一个最左推导,从左到右匹配输入的单词符号串。在某一步推导中,我们需要用非终结符A匹配输入符号a,如果A的产生式候选式αi的终结首符集包括a,则我们就可以用产生式A→αi构造最左推导,即用αi代替A。

(1)预测分析表的概念和构成

  1. ​预测分析表​​:是一个M[A, a]形式的矩阵,其中A为非终结符,a是终结符或‘#’(#作为输入串的结束符,在分析输入串之前添加到输入串尾部)。矩阵元素M[A, a]中存放着一条关于A的产生式,指出当A面临输入符号a时所应采用的候选式。M[A, a]中也可能存放一个“出错标志”,指出A根本不该面临输入符号a
  2. 编译原理(1)—— 语法分析(自上而下)_编译原理_42

  3. 这个示例中,第一列给出了所有非终结符;第一行给出了所有可能的输入符号;矩阵内每个元素代表用此非终结符匹配此输入符时应该使用的产生式;空白代表出错标志。
  4. 思想:

    1. 我们知道用LL(1)文法进行自上而下的语法分析是一个非回溯过程,从起始符开始的每一步推导都是唯一确定的(LL(1)文法的M[A,a]表中每个格最多一个产生式)。
    2. 假定A→α是一个产生式,a∈FIRST(α),则当A呈现在STACK栈之顶且a是当前输入符号时,α应被当作A的唯一合适的全权代表(可以确认A→α是当前唯一可行的推导)。因此M[A,a]中应该放进产生式A→α。
    3. 编译原理(1)—— 语法分析(自上而下)_语法分析_43时,如果当前面临的输入符号a(可能是终结符或‘#’)属于FOLLOW(A),那么,A→α就认为已获得自动匹配,因而,应把A→α放在M[A,a]中。
  5. 构造预测分析表

    • 对文法G的每个产生式​​A→α​​执行第二步和第三步;
    • 对每个终结符​​a∈FIRST(α)​​,把​​A→α加到M[A,a]​​中;
    • 若​​ε∈FIRST(α)​​,则对任何​​b∈FOLLOW(A)​​,把​​A→α加到M[A,b]​​中;
    • 把所有无定义的​​M[A,a]​​标上​​出错标志​
  6. 例子:对于文法E→TE’,E’→+TE’|ε,T→FT’,T’→*FT’|ε,F→(E)|i:
    FIRST(E) ={ (,i} ; FOLLOW(E) = { ),#}
    FIRST(E’)={ +,ε} ; FOLLOW(E’) = { ),#}
    FIRST(T) ={ (,i} ; FOLLOW(T) = { +,) ,#}
    FIRST(T’)={ *,ε} ; FOLLOW(T’) = { +,),#}
    FIRST(F) ={ (,i} ; FOLLOW(F) = { *,+,),#}
    这个文法构造的预测分析表即为上面那个蓝色的示例

(2)预测程序的工作过程

  • 对于LL(1)文法的分析使用一个预测分析表和一个栈来进行联合控制。栈用来存放文法符号,预测分析表指出当栈顶文法符号面临输入符号时应该采取的动作

  • 预测分析模型
  • 编译原理(1)—— 语法分析(自上而下)_递归_44

  • 预测分析程序的工作过程:

    1. 开始工作时,栈中首先放入‘#’号和文法的开始符号,给输入串尾部添加’#'号。
    2. 预测分析程序的总控程序在任何时候都是按栈顶符号X当前的输入符号a行事的。对于任何(X,a),总控程序每次都执行下述三种可能的动作:
      1. 若X=a=‘#’,则宣布分析成功,停止分析过程;
      2. 若X=a≠‘#’,则把X从栈顶逐出,让a指向下一个输入符号;
      3. 若X是一个非终结符号,则查看分析表M。若M[A,a]中存放着关于X的一个产生式,那么首先把X逐出STACK栈顶,然后把产生式的右部符号串按反序一一入栈(若右部符号为ε,则意味没有符号推进栈)。若M[A,a]中存放着“出错标志”,则调用出错处理程序。
  • 出错处理:

    1. 预测分析过程中出现下面两种情况就遇到了语法错误:
      1. 栈顶的终结符与当前的输入符号不匹配。
      2. 非终结符A处于栈顶,面临的输入符号为a,但预测分析表M中的M[A,a]为空。
    2. 处理方法:跳过剩余输入符号中的若干符号,使得栈和剩余输入符号串能重新协调的同步工作。
      1. 在分析表中加入同步符号M[A,FOLLOW(A)] = “synch”。
      2. 在分析中,查到入口M[A,a]若是空白,则跳过输入符号a;若是synch,则从栈中将非终结符A弹出,如果栈顶的终结符与输入符号不匹配,则将此终结符从栈中弹出。