程序员不得不会的计算机科班知识——编译原理篇(中)

490 阅读23分钟

计算机科班知识整理专栏系列文章:

【1】程序员不得不会的计算机科班知识——操作系统篇(上)
【2】程序员不得不会的计算机科班知识——操作系统篇(下)
【3】程序员不得不会的计算机科班知识——数据库原理篇(上)
【4】程序员不得不会的计算机科班知识——数据库原理篇(下)
【5】程序员不得不会的计算机科班知识——数据结构与算法篇(上)
【6】程序员不得不会的计算机科班知识——数据结构与算法篇(下)
【7】程序员不得不会的计算机科班知识——软件工程篇(上)
【8】程序员不得不会的计算机科班知识——软件工程篇(中)
【9】程序员不得不会的计算机科班知识——软件工程篇(下)
【10】程序员不得不会的计算机科班知识——编译原理篇(上)
【11】程序员不得不会的计算机科班知识——编译原理篇(中)
【12】程序员不得不会的计算机科班知识——编译原理篇(下)
【13】程序员不得不会的计算机科班知识——计算机网络篇(上)
【14】程序员不得不会的计算机科班知识——计算机网络篇(中)
【15】程序员不得不会的计算机科班知识——计算机网络篇(下)

第三章 上下文无关文法与语法分析

3.1 基本概念

3.1.1 语法分析

①程序设计语言的语法通常是用上下文无关文法的文法规则进行描述的

②与正则表达式最大的区别:上下文无关文法能够表达递归

③语法分析的任务:根据扫描程序产生的记号来确定程序的语法结构

④语法分析阶段的输出:分析树或者语法树

3.1.1 文法

定义:文法是描述语言的语法结构的形式规则(G(S))

3.2 上下文无关文法

上下文无关文法说明程序设计语言的语法结构。除了涉及到了递归规则之外,其他的说明与使用正则表达式的词法结构的说明十分类似。

①终结符:产生式中没有出现在左侧的符号

②非终结符:通常出现在产生式左侧,在语法推导中总是被替换

③开始符号:一般是文法规则中第一条产生式左侧的非终结符

3.2.1 推导

推导(derivation)是指在文法规则的右边选择一个序列来替换左侧的非终结符。推导以一个非终结符开始并以终结符串结束。在推导的每一个步骤中,使用来自文法规则的选择生成一个替换。通常用符号”=>”表示。

  • 句型:如果开始符号S可经过任意步推出α,则称α是一个句型。
  • 句子:仅含终结符的句型
  • 语言:文法G产生的句子的全体

例子:

3.2.2 递归

①左递归:左侧非终结符出现在右侧第一个位置。(如:A ->A a | a)

② 右递归:左侧非终结符出现在右侧最后一个位置。(如:A->a A | a)

例题:

例1:

对于 A -> Aα | β,α 和β表示字母表上的任意串,其中β不能以A开头,

(1)思考这个文法表示的语言是?βα*

(2)如果换成右递归来写还是表示一样的语言吗?不是。 A -> αA | β 表示α*β

例2:

例3:

例4:

例5:

例6:

3.3 分析树与推导

①一个串的推导过程可能并不是唯一的,有最左推导、最右推导和既非最左推导又非最右的推导

②最左推导:是指每一步中最左的那个非终结符都要被替换的推导

③最右推导:是指每一步中最右的那个非终结符都要被替换的推导

④用树形结构表示推导过程就可以得到串的分析树:

a. 叶子节点:终结符

b. 其他节点:非终结符

c. 根:开始符号

三种推导对应的分析树是一样的,只是遍历的顺序不同。

例1:

例2:

例3:

3.4 语法树

语法树:

a. 叶子节点是:参与运算的数

b. 其他节点:表示运算

!!!优先级仍然保留在树形结构中

同一个语言,描述的语法规则的改变会影响到分析树的变化,但是却不会影响到语法树的表示。

例子:

3.5 二义性文法

3.5.1 二义性文法的概念及判断

定义:该文法允许一个串对应多个分析树,存在串对应着两个最左推导或两个最右推导。

注意:文法二义并不代表语言一定是二义的,只有当产生一个语言的所有文法都是二义时,这个语言才成为二义的。

例子:

为句子abab构造两个不同的最左推导,以此说明该文法是二义的:

第一种最左推导:

S → a S b S → a b S a S b S → a b a S b S → a b a b S → a b a b S→aSbS→abSaSbS→abaSbS→ababS→ababS→aSbS→abSaSbS→abaSbS→ababS→abab

第二种最左推导:

S → a S b S → a b S → a b a S b S → a b a b S → a b a b S→aSbS→abS→abaSbS→ababS→ababS→aSbS→abS→abaSbS→ababS→abab

因此该文法是二义的。

3.5.2 如何消除文法的二义性

例子1:
有下列描述命题演算公式的二义文法,为它写一个等价的非二义文法

SS and SS or Snot Spq ∣(S)

由于该文法没有体现算符and、or、not的优先次序和结合规则,因此该文法二义。如p and q or p可以分解成两个子表达式,见下图的两棵不同语法树。

从我的理解来看,消除二义性,无非就是体现出各算符的优先次序,因此我总结如上形式的消除二义方法如下:(先将优先级相同的运算归为一组)

  1. 将最低优先级的运算提至第一层产生式
  2. 最后一层上的各层产生式添加′ ∣ ′单独推导向下一层
  3. 其余按照优先级高低逐层向下写
  4. 使用新的非终结符代替原终结符
  5. 最后一层产生式要能够推回第一层产生式

则上述二义文法消除二义性如下:

EE or TT

TT and FF

Fnot Fpq ∣(E)

做一个简要的解释:因为or的优先级是最低的,not最高,所以第一层是or运算的产生式,可以看到最后一层存在(E),括号保证了or运算的分解是唯一的。

例子2:

改写二义文法

E → E+E|E ∗ E |(E )| − E ∣ id

优先级从低到高:[ + ] ; [ ∗ ] ; [ ( ) , − , id ]

结合性:

左结合:[ + , ∗ ]

右结合:[−]

无结合:[id]

非终结符与运算:

E : +(E产生式,左递归)

T : ∗(T产生式,左递归)

F : − , ( ) , id(F产生式,右递归)

得到:

E→E+T∣T

T→T∗F∣F

F→(E)∣−F∣id

3.6 EBNF与乔姆斯基语言分类

3.6.1 EBNF

前面用来表示上下文无关文法规则的形式被称为BNF(Backus-Naur form)。EBNF是BNF的扩展表示方法。

3.6.1.1 EBNF中的重复

在BNF中,重复是使用递归表示的,重复实际分两种:嵌套重复和并列重复,并列重复对应到程序是可以用循环来实现的。为了更方便的表示并列重复,对BNF进行扩展。

例子:

3.6.1.2 EBNF中的可选

另一种EBNF扩展的表示方式是可选,使用[...]表示。

3.6.2 乔姆斯基语言分类

·3型语言(正则文法)

·2型语言(上下文无关文法)

·1型语言(上下文有关文法)

·0型语言(短语文法,图灵机)

包含关系,比如1型文法不能表示的语言,2型文法也不能表示。

3.7 语法树和语法概念的关系

3.7.1 子树与简单子树

  • 子树:某语法树T中的某一节点A和它所有分支组成的树T',则称T'是T的一颗子树。(包含整棵树)
  • 简单子树:某一节点A与其子节点(单层节点)组成的树。从严格意义上来说,A应该有且仅有一层子节点。
  • 子树和简单子树的区别就在于层数的不同,简单子树就是倒数第二层,子树是从哪开始都可。

3.7.2 句型、短语、简单短语、句柄

  • 语法树的所有叶节点(从左到右)组成的是一个句型
  • 语法树的子树所有叶节点(从左到右)组成的是一个短语
  • 简单子树所有叶节点(从左到右)构成一个简单短语
  • 最左简单子树叶节点对应的是一个句柄

问题:由给定的文法规则判断给定的串是否为句型,有哪些短语、简单短语、哪个是句柄。

解决方法:先画出语法树,从起始符开始,观察语法树如何生长(即当前树的向下生长使用哪条规则),然后画出语法树,如果最后所有叶子节点从左到右和给的串一致,则当前串是句型。每一棵子树的叶子节点对应的都是短语,单层的就是简单短语,最左层的就是句柄。

例题:

对于上述文法,有:

短语:

(T+i)*F+i; //整个的肯定是短语由E推导出来

(T+i)*F; //是由T推出来的

两个i ;

(T+i);

T;

T+i;

简单短语:

T;

两个i;

句柄:

T;

第四章 自顶向下的分析

4.1 概述

4.1.1 引入

自顶向下(top-down)的分析算法通过在最左推导中描述出来的各个步骤来分析记号串的输入,有时也说自上而下的分析算法。

对任意的输入串,自顶向下的分析方法都是从开始符号开始,向下推导,直至生成输入串句子或者发生错误。这意味着分析试图从根节点开始向叶子节点构造一棵语法树。本质上是一种试探的分析过程。

自顶向下的分析算法主要分为两类:

  • 回溯分析程序(backtracking parser)
  • 预测分析程序(predictive parser)

回溯分析比预测分析强大,但通常都非常慢,对构造实际的编译器并不合适。本章介绍的都是预测分析方法。

预测分析方法有两种:

  • 递归下降分析(recursive-decent pasering):适合手工构造分析程序。
  • LL(1)分析(LL(1) pasering):带有显式栈的分析方法。

4.1.2 三个重要的集合

4.1.2.1 First集(alpha短语)

根据串进行定义的,可以根据一个串来求first集,指的是能用这个串推出来的所有可以作为终极符的集合。推出以哪些终极符作为开头的串,这些终极符就是first集,如果能够推出null,null也是first集中的元素。

关于First(X)集的计算规则:

如果是一个字符的话,直接按照规则进行运算即可;

如果是一个字符串的话,如果第一个字符的First集中没有null,第一个字符的first集就是整个字符串的first集,如果第一个字符串的First集中有null 的话就是第一个元素的first集减去null与第二个字符的first集取并集,直到当前字符的first集中不包含null或整个字符串遍历完毕。

《定义计算》对每一文法符号X∈V计算First(X)

(1)若X∈VT,则FIRST(X)={X}(如果X是终极符,直接填入到First集合中)

(2)若X∈VN,且有产生式X→a…,a∈VT, 则 a∈FIRST(X)(如果X是非终极符,但是经过简单推导一步就可以推导到终极符a,则a也属于First集合)

(3)若X∈VN,X→ε,则ε∈FIRST(X)

如果能够推出null,则null也属于first集。(如果推导出了空集,则空集属于First集合)

(4)若X, Y1,Y2,…,Yn都∈VN,且有产生式X→Y1 Y2 …Yn;当Y1 Y2 … Yi-1都ε时,(其中1≤i≤n),则FIRST(Y1)-{ε}、FIRST(Y2) -{ε} 、…、FIRST(Yi-1)- {ε},FIRST(Yi)都包含在FIRST(X)中(如果推导出的是一连串的非终极符,那么每个非终极符的First在去掉ε之后的并集都属于当前的符号的First集合中)

(5)一直推导到最后一个不包含ε,或者直接到整个文法推导结束,反复执行上述步骤

例1:

文法G [S]为:

S→AB

S→bC

A→ε

A→b

B→ε

B→aD

C→AD

C→b

D→aS

D→c

求每个非终结符的First集?

解:

对于非终结符S,S的开始符有终结符b和非终结符A,根据上面定义②把b加入FIRST(S),由上面的定义③把FIRST(A)中的所有非空符号串ε元素都加入到FIRST(S)中,而且因为S→AB符合上面的定义④和⑤,所以FIRST(S)=(FIRST(A)-{ε})∪(FIRST(B) -{ε})∪{ε}那么先求FIRST(A)和FIRST(B),

A的开始符有终结符b和空符号串ε,由定义①和定义②全部加入FIRST(A),也就是FIRST(A)={ε,b} ,同理,FIRST(B)={a,ε} ,这样FIRST(S)也就求出来了,FIRST(S)={b,a,ε} ,其实回过头来看之所以要反复调用定义②~⑤,是因为开始符中A可能为空字符串ε,所以要考虑A后面的情况。

再求,FIRST(C),由定义①,把b加入FIRST(C),再由定义④,FIRST(C)=(FIRST(A)-{ε} )∪{FIRST(D)},先求FIRST(D),易知,FIRST(D)={a,c} ,所以FIRST(C)={b,a,c}

例2:

文法G [E]为:

E->TE'

E'->+TE'|ε

T->FT'

T'->*FT'|ε

F->(E)|i

求每个非终结符的First集?

解:

1、第一次查看所有式子

2、首先看第一个式子:E → TE′,候选式中未包含终结符,暂时无法得出结果

当前First集:

First(E) = { }

First(E′) = { }

First(T) = { }

First(T′) = { }

First(F) = { }

3、第二个式子:E'->+TE'|ε,可以得到其所有候选式的串首终结符: +、ε,将其加入到First(E′)中

当前First集:

First(E) = { }

First(E′) = {+,ε}

First(T) = { }

First(T′) = { }

First(F) = { }

4、第三个式子同 1,First集无变化

5、第四个式子同 2,将 *、ε加入到First(T′)中

当前First集:

First(E) = { }

First(E′) = {+,ε}

First(T) = { }

First(T′) = { *,ε}

First(F) = { }

6、第五个式子:F->(E)|i,将 (、i 加入到First(F)中

当前First集:

First(E) = { }

First(E′) = {+,ε}

First(T) = { }

First(T′) = { *,ε}

First(F) = { (,i}

7、第二次查看所有式子

第一个式子可得,First(T) 是First(E) ,但First(T)是空的,故无变化

(注:若有X → Y且Y是非终结符,就把First(Y)中非ε的元素加入到First(X)中)

8、第二个式子不能得到新的结果,下一步

9、第三个式子可得,First(F)是First(T),故将 ( ,i 加入到First(T)

当前First集:

First(E) = { }

First(E′) = {+,ε}

First(T) = { (,i }

First(T′) = { *,ε}

First(F) = { (,i }

10、后续式子均不能得到新的结果,下一步

11、第三次查看所有式子

12、第一个式子得,First(T) 是First(E),故First集产生变化

当前First集:

First(E) = {(,i }

First(E′) = {+,ε}

First(T) = { (,i }

First(T′) = { *,ε}

First(F) = { (,i }

13、后续式子均无法得到新的结果,求First集完毕

注:理论上每次First集产生变化就要重新查看一遍所有式子,这里为了方便进行了简写,实际做题时可以按照变化一次重新求一次

4.1.2.2 Follow集(A非终极符)

存在这样一个句型,可以出现在非终极符后面出现第一个终极符,就属于follow集。

关于Follow集的计算规则:

①设S为文法中开始符号,把{#}加入FOLLOW(S)中(这里“#”为句子括号)。

②若A→αBβ是一个产生式,则把FIRST(β)的非空元素加入FOLLOW(B)中。如果β能够推导出ε则把FOLLOW(A)也加入FOLLOW(B)中。

③反复使用(b)直到每个非终结符的FOLLOW集不再增大为止。

例1:

文法G [E]为:

E → TE'

E' → +TE' | ε

T → FT'

T' → *FT' | ε
F → (E) | i

求每个非终结符的FOLLOW集?

解:先给出它们的FIRST集:(求解方法见上面FIRST集的求解)

First(E) = {(,i }

First(E′) = {+,ε}

First(T) = { (,i }

First(T′) = { *,ε}

First(F) = { (,i }
FOLLOW集的求解:

因为E是文法的识别符所以把#加入FOLLOW(E),又由规则F → (E) | i 得E的后跟符号),所以,FOLLOW(E)={ #,) };
FOLLOW(E’)={ #,) } ∵E → TE’ ∴FOLLOW(E)加入 FOLLOW(E’)
FOLLOW(T)={+,),#} ∵E'→ +TE’ ∴FIRST(E’)-{ε}加入FOLLOW(T); 又E’→ε, ∴ FOLLOW(E’)加入FOLLOW(T)
FOLLOW(T’)= FOLLOW(T)= {+,),#} ∵T → FT’ ∴ FOLLOW(T)加入FOLLOW(T’)
FOLLOW(F)={*,+,),#} ∵T → FT’ ∴ FOLLOW(F)=FIRST(T’)-{ε} ; 又T’→ε ∴ FOLLOW(T)加入FOLLOW(F)

例2:

文法G [E]为:

E → TE'

E' → +TE' | ε

T → FT'

T' → *FT' | ε
F → (E) | i

求每个非终结符的FOLLOW集?

解:先给出它们的FIRST集:(求解方法见上面FIRST集的求解)

First(E) = {(,i }

First(E′) = {+,ε}

First(T) = { (,i }

First(T′) = { *,ε}

First(F) = { (,i }
FOLLOW集的求解:

1、将#置入Follow(文法开始符号),即Follow(E)

当前Follow集:

Follow(E) = { # }

Follow(E′) = { }

Follow(T) = { }

Follow(T′) = { }

Follow(F) = { }

2、第一次查看所有式子

3、从E → TE′可得

Follow(E)可以加入到Follow(E′),将 # 置入Follow(E′)

Follow(E)可以加入到Follow(T),将 # 置入Follow(T)

[注:因为E′可能为ε,则First(E')中非ε元素可以加入到Follow(T)中,将 + 置入Follow(T)]

当前Follow集:

Follow(E) = { # }

Follow(E′) = { # }

Follow(T) = { #,+ }

Follow(T′) = { }

Follow(F) = { }

4、从E ′ → +TE′∣ε可得FIRST(E’)-{ε}加入FOLLOW(T)、 FOLLOW(E’)加入FOLLOW(T),无变化

5、从T → FT′可得Follow(T)可以加入到Follow(T′),将 #、+ 置入Follow(T′),Follow(T)可以加入到Follow(F),将 #、+ 置入Follow(F)

又因为T’可以推导出ε,则First(T′)中非ε元素可以加入到Follow(F)中,将 * 置入Follow(F)

当前Follow集:

Follow(E) = { # }

Follow(E′) = { # }

Follow(T) = { #,+ }

Follow(T′) = { #,+ }

Follow(F) = {#,+ ,* }

6、从T′ → ∗ F T ′ ∣ ε可得Follow(T′)可以加入到Follow(F),无变化

从F → ( E ) ∣ i可得非终结符E后有一个终结符 ),故将)置入Follow(E)

当前Follow集:

Follow(E) = { #,) }

Follow(E′) = { # }

Follow(T) = { #,+ }

Follow(T′) = { #,+ }

Follow(F) = {#,+ ,* }

7、第二次查看所有式子

从E → TE′可得Follow(E)可以加入到Follow(E′),将)置入Follow(E′)

Follow(E)可以加入到Follow(T),将 ) 置入Follow(T),First(E′)中非ε元素可以加入到Follow(T)中,已有+,Follow(T)无变化

8、当前Follow集:

Follow(E) = { #,) }

Follow(E′) = { # ,) }

Follow(T) = { #,+ ,) }

Follow(T′) = { #,+ }

Follow(F) = {#,+ ,* }

9、从E ′ → + T E ′ ∣ ε可得Follow(E′)可以加入到Follow(T),无变化

从T → F T′可以得到Follow(T)可以加入到Follow(T′),将 ) 置入Follow(T′)

Follow(T)可以加入到Follow(F),将 )置入Follow(F)

First(T′)中非ε元素可以加入到Follow(F)中,已有*,Follow(F)无变化

当前Follow集:

Follow(E) = { #,) }

Follow(E′) = { # ,) }

Follow(T) = { #,+ ,) }

Follow(T′) = { #,+,) }

Follow(F) = {#,+ ,*,) }

10、从T ′ → ∗ F T ′ ∣ ε可得Follow(T′)可以加入到Follow(F),无变化

从F → ( E ) ∣ i并不能得到新的结果

11、再次查看所有式子,均不能产生新的变化,求Follow集完毕

注:理论上每次Follow集产生变化就要重新查看一遍所有式子,这里为了方便进行了简写,实际做题时可以按照变化一次重新求一次

4.1.2.3 Predict集(A->alpha这样一个规则)

如果first(alpha)中没有null直接就是Predict集,如果有null(First(alpha)-null)UFollow(A)

Predict集合是针对整条文法语句的,如果->右侧的第一个是终极符,则直接就等于这个终极符,如果不是终极符,先找这个非终极符的First集,如果里面没有ε,则Predict就等于这个First集,如果里面有ε,则Predict等于First集减去ε,再加上当前的起始推导字符的Follow集。

例子:

4.2 LL(1)语法分析

4.2.1 LL(1)语法分析原理

  • 基本思想:从左到右扫描,按最左推导的方式推出输入流LL(1)文法
  • 定义:对于文法G中任一非终极符A,其任意两个产生式A→>α和A→>β,都要满足下面条件:Predict(A→a)∩Predict(A→β)=∅
  • LL(1)文法定义中的条件是为了满足选择规则时的唯一性,在我们选择规则的时候只能选择其中的一条规则进行推导,依据这样的思想构造的语法分析程序叫做LL(1)语法分析程序
  • 一个不是LL(1)文法并不一定说明它是二义性文法,但是通常会存在二义性。
  • 语法分析的动作:
    • 替换:当分析栈的第一个符号是VN时,就要用规则进行推导,用规则右部将其替换(替换就是直接使用右部代替左部,而待判断字符串不变)
    • 匹配:分析栈第一个符号是VT时,与输入流中的第一个符号进行匹配(匹配需要分析栈和字符串同时改变)
    • 成功
    • 出错、失败
  • 整个语法分析器的结构:stack就是所谓的栈,栈顶是X1,分析的对象是输入流,这是我们用到的两个分析对象:VT是终极符集合(Terminal),非终极符集合。

4.2.2 LL(1)分析表的构造

终极符VT里还要增加一个#,竖着写非终极符,横着写终极符。

给每个文法规则编号,随便编,然后在E的predict集中有某个终极符的框框里填文法规则的编号,没有的地方空着就行。

例如:

4.2.3 驱动程序设计

  • 初始格局的S是初始状态,后面是带匹配的字符串
  • 一般格局是指驱动程序执行一段时间之后,当前的结果状态。前面是根据文法规则推导出来的一些短语,后面是前面已经被部分匹配掉的字符串。
  • 每步的分析:
    • 如果X1,最开头的元素是终极符就要和待比较字符串进行匹配,匹配成功就从两个列表中把它们去掉
    • 如果不是终极符,根据当前文法规则使用右部代替左部,注意这里涉及到压栈顺序的问题
    • 最后只剩两个#则成功,否则报错还要提供相关的错误信息。

例如:

在上述LL(1)分析表的基础上,假设有输入串i+i*i,写出相对应的LL(1)分析程序的动作。

解:(矩阵元素这列的下标为S、T的串的第一个元素,右部代替左部时用对应数字标号的推导式)

4.2.4 满足LL(1)文法分析的条件

  1. 没有公共前缀
    A->aB|aC|aj|a
    这些项都有一个共同的左公共因子,可以把它提取出来
    (和数学中提取公因式相同,如果最后一个是a的话,提取出来剩一个null)
    A->aA'
    A'->B|C|j|null
  1. 消除左递归

左递归有两种:直接左递归和间接左递归

    1. 直接左递归
    A->Aa|b
    消除方法:
    A->bA'
    A'-> aA'|null(不要忘了这个null)
    2. 间接左递归
    A->Bb
    B->Aa
    先转换成直接左递归,就将B->Aa代入到上面的式子中即可
    A->Aab
    然后利用化简直接左递归的方法进行化简。

即按照LL(1)文法的定义来进行判断,如果两个推出的predict集有交集就不符合LL(1)文法。(相同左部产生式的predict交集)

4.3 递归下降分析

4.3.1 递归下降法的基本原理

以下图为例:

先不考虑左递归的问题,在定义语法分析程序的时候,每一个非终极符都定义成一个过程或者函数:

1、每棵子树都是以根节点的非终极符推导出来的短语

2、可以考虑每个非终极符构造一个函数,去匹配子树的叶节点

从树中即可看出,加入每一个非终极符都定义成一个过程或者是函数,选择一个规则的时候就让它和规则的右边进行匹配,遇到终极符就可能直接匹配上了,遇到非终极符还是要调用该非终极符所对应的过程或者是函数。

对文法的要求:( 为了保证推导的唯一性,对文法的要求与LL(1)文法相同。)

文法不能是直接左递归的

以一个非终极符为左部的任意规则的predict集,交集为空。

实际上这里就是一个条件:交集为空,因为含有直接左递归的文法一定不满足第二个条件,间接左递归也不行

4.3.2 语法分析程序的构造

两个标准函数:

  1. ReadToken:把输入流的头符读入变量token中
  2. Match(a): if token=a then ReadToken else 出错

程序中有一个全局变量token,用来存储输入流的第一个语法符号,分析的时候需要一个一个往里读,token保存当前字符串的第一个字符;遇到非终极符的时候有一个匹配的动作即Match(a),如果满足Match后面的函数的内容可以读取下一个语法符号了。

4.3.2.1 非终极符的过程或者函数的写法

    // 有规则:Stm-> while Exp do Stm
    // 对应产生式右部的语法分析程序部分如下:
    begin
    	Mathch($while);
        Exp();
    	Mathch($do);
    	Stm();
    end

    // 例如:while x>y do if x>z then x:=x+y else:=y

4.3.2.2 子程序构造方法

  1. 同一个非终极符推导出来的规则写在一个函数中,每个规则的Predict集作为if条件判断中的布尔表达式的一部分(这里也体现了第二条规则的满足,即没有交集);
  2. 对于终极符,直接执行Match部分
  3. 如果当前的X属于空集,则当前执行的语句是空语句

4.3.2.3 主程序构造方法

  1. 执行ReadToken();把字符串读入
  2. 执行开始符对应的子程序
  3. 进行终止判断(就是#部分的判断)
    // 主程序
    void main(){
    	ReadToken();S();
        if(token=='#') 成功;
        else 失败
    }

4.3.2.4 整体的问题解决方法

具体构建流程:(给定一个文法G)

  1. 求每条规则的Predict集
  2. 写针对每个非终极符的函数
  3. 写主函数

优点:构造简单

缺点:

  1. 频繁的函数调用影响效率
  2. 程序比较长

4.3.3 相关例题

1、先求每条规则的Predict集

2、写针对每个非终极符的函数

    E(){
    if token ∈ {i,(}
    then{
     T();
     E'();
    }
    }

    E'(){
    if token ∈ {+}
    then{
     match(+);
     T();
     E'();
    }
    if token ∈ {#,)}
    then skip;
    }

    T(){
    if token ∈ {i,(}
    then {
    F();
    T'();
    }
    }

    T'(){
    if token ∈ {*}
    then {
    match(*);
    F();
    T'();
    }
    if token ∈ {+,#,)}
    then skip;
    }

    F(){
    if token ∈ {(}
    then{
    match('(');
    E();
    match(')');
    }
    if token ∈ {i}
    then match(i);
    }

    main(){
    ReadToken();
    E();
    if(token=='#')
      return true;
    else
      return false;
    }

3、同时也可画出该串的语法分析树