C语言
实验环境:Visual Studio 2019
author:zoxiii
预测分析法(1)构造分析表
1、实验内容
实现给定文法的预测分析表。
- 输入:某一文法(可以使用书中的例题)
- 输出:该文法对应的LL(1)分析表
2、前期准备
2.1 LL(1)分析法的原理
L
L
(
1
)
LL(1)
LL(1)分析法又称预测分析法,是一种不带回溯的飞度贵自顶向下分析法。
L
L
(
1
)
LL(1)
LL(1)的含义是:第一个L表明自顶向下分析时从左至右扫描输入串的;第二个
L
L
L表明分析过程中将用最左推导;
“
1
”
“1”
“1”表明只需向右查看一个符号就可决定如何推导(即可知用哪个产生式进行推导)。
L
L
(
1
)
LL(1)
LL(1)分析法的基本思想是根据输入串的当前输入符号来唯一确定选用某条规则(产生式)来进行推导;当这个输入符号与推导的第一个符号相同时,再取输入串的下一个符号,继续确定下一个推导应选的规则;如此下去,直到推导出被分析的输入串为止。
2.2 构造FIRST、FOLLOW集合的过程
在表驱动的
L
L
(
1
)
LL(1)
LL(1)分析器中,除了分析表因文法的不同而异之外,分析栈、控制程序都是相同的。因此,构造一个文法的表驱动
L
L
(
1
)
LL(1)
LL(1)分析器实际上就是构造该文法的分析表。
为了构造分析表
M
M
M,需要预先定义和构造两族与文法有关的集合
F
I
R
S
T
FIRST
FIRST和
F
O
L
L
O
W
FOLLOW
FOLLOW。
(1)
F
I
R
S
T
FIRST
FIRST集合的构造方法:对文法中的每一个非终结符
X
X
X构造
F
I
R
S
T
(
X
)
FIRST(X)
FIRST(X),其方法是连续使用下述规则,直到每个集合的
F
I
R
S
T
FIRST
FIRST不再增大为止。(注:对终结符
a
a
a而言,
F
I
R
S
T
(
‘
a
’
)
=
{
a
}
FIRST(‘a’)=\{a\}
FIRST(‘a’)={a},因而无需构造。)
①若有产生式
X
−
>
a
⋅
⋅
⋅
X->a···
X−>a⋅⋅⋅,且
a
∈
V
T
a∈V_T
a∈VT,则把
a
a
a加入到
F
I
R
S
T
(
X
)
FIRST(X)
FIRST(X)中;若存在
X
−
>
ε
X->ε
X−>ε,则将
ε
ε
ε也加入到
F
I
R
S
T
(
X
)
FIRST(X)
FIRST(X)中。
②若有
X
−
>
Y
⋅
⋅
⋅
X->Y···
X−>Y⋅⋅⋅,且
Y
∈
V
N
Y∈V_N
Y∈VN,则将
F
I
R
S
T
(
Y
)
FIRST(Y)
FIRST(Y)中的所有非
ε
ε
ε元素(记为
“
∖
{
ε
}
”
“\setminus\{ε\}”
“∖{ε}”)都加入到
F
I
R
S
T
(
X
)
FIRST(X)
FIRST(X)中,若有
X
−
>
Y
1
Y
2
⋅
⋅
⋅
Y
k
X->Y_1Y_2···Y_k
X−>Y1Y2⋅⋅⋅Yk,且
Y
∼
Y
Y \sim Y
Y∼Y都是非终结符,而
Y
1
∼
Y
i
Y_1 \sim Y_i
Y1∼Yi的候选式都有
ε
ε
ε存在,则把
F
I
R
S
T
(
Y
j
)
(
j
=
1
,
2
,
⋅
⋅
⋅
,
i
)
FIRST(Y_j)(j=1,2,···,i)
FIRST(Yj)(j=1,2,⋅⋅⋅,i)的所有非
ε
ε
ε元素加入到
F
I
R
S
T
(
X
)
FIRST(X)
FIRST(X)中;特别是当
Y
1
∼
Y
k
Y_1 \sim Y_k
Y1∼Yk均含有
ε
ε
ε产生式时,应把
ε
ε
ε也加到
F
I
R
S
T
(
X
)
FIRST(X)
FIRST(X)中。
(2)
F
O
L
L
O
W
FOLLOW
FOLLOW集构造方法:对文法
G
[
S
]
G[S]
G[S]的每个终结符
A
A
A构造
F
O
L
L
O
W
(
A
)
FOLLOW(A)
FOLLOW(A)的方法是连续使用下述规则,直到每个
F
O
L
L
O
W
FOLLOW
FOLLOW集合不再增大为止。
①对文法开始符号
S
S
S,置#于
F
O
L
L
O
W
(
S
)
FOLLOW(S)
FOLLOW(S)中(由语句括号“#
S
S
S#”中的
S
S
S#得到)。
②若有
A
−
>
α
B
β
A->αBβ
A−>αBβ(
α
α
α可为空),则将
F
I
R
S
T
(
β
)
∖
{
ε
}
FIRST(β)\setminus\{ε\}
FIRST(β)∖{ε}加入到
F
O
L
L
O
W
(
B
)
FOLLOW(B)
FOLLOW(B)中。
③若有
A
−
>
α
B
A->αB
A−>αB或
A
−
>
α
B
β
A->αBβ
A−>αBβ,且
β
=
>
ε
β=>ε
β=>ε(即
ε
∈
F
I
R
S
T
(
β
)
ε∈FIRST(β)
ε∈FIRST(β)),则把
F
O
L
L
O
W
(
A
)
FOLLOW(A)
FOLLOW(A)加到
F
I
R
S
T
(
B
)
FIRST(B)
FIRST(B)中(此处的
α
α
α也可以为空)。
构造
F
I
R
S
T
FIRST
FIRST集和
F
O
L
L
O
W
FOLLOW
FOLLOW集的过程有可能要反复进行很多次,直到每一个非终结符的
F
I
R
S
T
FIRST
FIRST集和
F
O
L
L
O
W
FOLLOW
FOLLOW集都不再增大为止。
2.3 构造LL(1)分析表的过程
F
I
R
S
T
FIRST
FIRST集确定了每一个非终结符在扫描输入串时所允许遇到的输入符号及所应采用的推导产生式(该非终结符所对应的产生式中的哪一个候选式)。
F
O
L
L
O
W
FOLLOW
FOLLOW集是针对文法中形如“
A
−
>
ε
A->ε
A−>ε”这样产生式的,即在使用A的产生式进行推导时,面临输入串中哪些输入符号时则此时由一空字(即
ε
ε
ε)匹配而不出错;当然,此时的扫描指针仍指向当前扫描的输入符后上,并不向前推进。
所以构造分析表
M
M
M的步骤如下:
①对文法
G
[
S
]
G[S]
G[S]的每个产生式
A
−
>
α
A->α
A−>α执行以下②、③步,
②对每个终结符
a
∈
F
I
R
S
T
(
A
)
a∈FIRST(A)
a∈FIRST(A),把
A
−
>
α
A->α
A−>α加入到
M
[
A
,
a
]
M[A,a]
M[A,a]中,其中
α
α
α为含有首字符
a
a
a的候选式或为唯一的候选式。
③若
ε
∈
F
I
R
S
T
(
A
)
ε∈FIRST(A)
ε∈FIRST(A)(或文法中有
A
−
>
ε
A->ε
A−>ε的产生式)则对任何属于
F
O
L
L
O
W
(
A
)
FOLLOW(A)
FOLLOW(A)的终结符
b
b
b,将
A
−
>
ε
A->ε
A−>ε加入到
M
[
A
,
b
]
M[A,b]
M[A,b]中。
④把所有无定义的
M
[
A
,
a
]
M[A,a]
M[A,a]标记为“出错”。
一个文法
G
[
S
]
G[S]
G[S],若它的分析表
M
M
M不含多重定义入口,则称它是一个
L
L
(
1
)
LL(1)
LL(1)文法,它所定义的语言恰好就是它的分析表所能识别的全部句子。一个上下无关文法是
L
L
(
1
)
LL(1)
LL(1)文法的充分必要条件是:对每一个非终结符A的任何两个不同产生式
A
−
>
α
∣
β
A->α|β
A−>α∣β,有下面的条件成立:
(1)
F
I
R
S
T
(
α
)
∩
F
I
R
S
T
(
β
)
=
Ф
FIRST(α)∩FIRST(β)=Ф
FIRST(α)∩FIRST(β)=Ф;
(2) 假若
β
=
>
ε
β=>ε
β=>ε,则有
F
I
R
S
T
(
α
)
∩
F
I
R
S
T
(
A
)
=
Ф
FIRST(α)∩FIRST(A)=Ф
FIRST(α)∩FIRST(A)=Ф;
条件(1)意味着
A
A
A的每个候选式都不存在相同的首字符,由
L
L
(
1
)
LL(1)
LL(1)分析表的构造方法可知:它避免了在分析表的同一栏目内出现多个产生式的情况,即避免了多重入口。
条件(2)避免了在分析表的同一栏目内出现
A
−
>
α
A->α
A−>α和
A
−
>
ε
A->ε
A−>ε(这同样是多重入口)的情况。
2.4 需实现的函数
首先对实验要求实现的内容进行分析,首先确定以下要实现的函数子过程:
(1)void First(); //求文法的
F
I
R
S
T
FIRST
FIRST集
(2)void Follow(); //求文法的
F
O
L
L
O
W
FOLLOW
FOLLOW集
(3)void Print_first(); //打印文法的
F
I
R
S
T
FIRST
FIRST集
(4)void Print_follow(); //打印文法的
F
O
L
L
O
W
FOLLOW
FOLLOW集
(5)void Print_table(); //打印并存储
L
L
1
LL1
LL1分析表
3、分析过程
3.1 分析文法的推导过程
选择文法 G [ V ] G[V] G[V]来构造其对应的预测分析表:
G[V]: V -> N | N[E]
E -> V | V+E
N -> i
由于该文法包含左递归和回溯,所以首先改写文法,对文法消除左递归和回溯,得到新的文法 G ’ [ V ] G’[V] G’[V]如下:
G’[V]: V -> NT
T -> [E] | ε
E -> VM
M -> +E | ε
N -> i
接下来按照前面的构造
F
I
R
S
T
FIRST
FIRST集、
F
O
L
L
O
W
FOLLOW
FOLLOW集的方法,对改写后的文法分析得到对应的集合如下:
(1)
F
I
R
S
T
FIRST
FIRST集合
FIRST(V)={i}
FIRST(T)={[,ε}
FIRST(E)={i}
FIRST(M)={ε,+}
FIRST(N)={i}
(2) F O L L O W FOLLOW FOLLOW集合
FOLLOW(V)={#}+FIRST(M)/{ε}+FOLLOW(E)={#,+,]}
FOLLOW(T)=FOLLOW(V)={ #,+,]}
FOLLOW(E)={]}+FOLLOW(M)={]}
FOLLOW(M)=FOLLOW(E)={]}
FOLLOW(N)=FIRST(T)/{ε}+FOLLOW(V)={#,+,],[}
再根据 F I R S T FIRST FIRST集、 F O L L O W FOLLOW FOLLOW集构造对应的预测分析表如下:
i | [ | ] | + | # | |
---|---|---|---|---|---|
V | V->NT | ||||
T | T->[E] | T->ε | T->ε | T->ε | |
E | E->VM | ||||
M | M->ε | M->+E | |||
N | N->i |
对于其他的 L L ( 1 ) LL(1) LL(1)文法也是同样的分析过程,在后面的运行结果中可以验证。
3.2 主程序分析
在理解了构造 F I R S T FIRST FIRST集、 F O L L O W FOLLOW FOLLOW集的方法之后,可以知道它们不是一次循环流程判断就可以实现的,需要不断重复运算,才能得到最终的不再增大的集合,而且我们在运算、存储的过程中不能够有重复的字符,所以我们引入map和set类型的数据来定义对应的集合变量,对于不能第一次处理的字符存储到暂时变量中,等待后续处理,由此建立了如下变量:
map<char, set<char>>first; //first集合
map<char, set<char>>follow; //follow集合
map<char, set<char>>first_temp;//暂存还需处理的first集合的字符
map<char, set<char>>follow_temp;//暂存还需处理的follow集合的字符
map<char, set<string>>string_temp;//存表达式
set<char>table; //终结符号
set<char>no_end; //非终结符
string s[10][10]; //分析表
在此基础上,主程序的流程图如图所示:
3.3 子程序流程分析
子程序中最重要的就是求解 F I R S T ∖ F O L L O W FIRST\setminus FOLLOW FIRST∖FOLLOW集合的过程,其中求解 F I R S T FIRST FIRST集、 F O L L O W FOLLOW FOLLOW集的流程图如下图所示:
3.4 源代码
3.5 运行结果
代码是通过读取文件中已经约定好的文法进行分析,输出结果,在实验中由于“
ε
ε
ε”不是西文字符,所以无法识别其对应的
A
S
C
I
I
ASCII
ASCII码,不好判断,所以我们用“ $ ”来代替“
ε
ε
ε”来实现。
由下图和前面的分析过程比较可得,代码输出的结果是正确的。
另外,对文法G[E]进行分析的结果如下图所示:
其中: G[E]: E -> TG
G -> +TG | $
T -> FS
S -> *FS | $
F -> (E) | i
4、遇到问题
(1)使用string类型的变量来存储集合的数据,不方便处理,查阅资料后将集合变量定义为map类型,便于插入新的字符、便于查找。
(2)再对于first_temp和follow_temp的后续处理过程中,因为map会对首非终结字符进行排序,所以也造成了用于赋值的集合并不完整,但已经被赋值了,所以只处理一次并不能得到最终正确的FIRST和FOLLOW集合,所以通过设置一定的次数,确保集合能够处理到不再增大为止,才是最终的结果。
(3)follow集的判断过程比较复杂,在实现的过程中耗时较长,最终得到的测试结果是正确的,但对于复杂的文法难免还会有未发现的错误。
参考文献:《编译原理教程 (第4版)》 胡元义 2016