前言
写起来真是脑壳疼,没有看到有blog详解和代码的,所以又双叒来了!
代码400行,注释就有200行(可能是我写过注释占比例最多的一次)。
项目地址
后续会更新LL,LR文法,提取左公因子和消除左递归在 src/struct/CFG.java
参考链接
在学习的过程中也发掘到几篇不错的blog
前文
在正文之前,先说几个封装的结构,以文法E举例:
E → E + T ∣ E − T E → E + T \ \ | \ \ E - T E→E+T ∣ E−T
一个产生式体集合 ; E E E 对应的ProductionSet有:{ E + T E + T E+T、 E − T E - T E−T }.
一个产生式体 ; E E E有两个产生式体: E + T E + T E+T、 E − T E - T E−T.
一个产生式子项, E + T E + T E+T有三个子项,分别是: E E E、 + + +、 T T T ; 每个子项都有一个子项属性, 此项是非终结符还是终结符 :
之所以封装成这样的结构,是为了在提取和消除时的方便,不然check时都是以字符串的形式,这样效率很低。
回顾单个文法的提取公因子和消除左递归
文法G,提取左公因子:
A → α β 1 ′ ∣ α β 2 ′ ∣ α β 3 ′ ∣ . . . ∣ α β n ′ ∣ γ A →αβ_1' \ \ | \ αβ_2' \ \ | \ αβ_3' \ \ | \ ... \ |αβ_n' \ \ | \ γ A→αβ1′ ∣ αβ2′ ∣ αβ3′ ∣ ... ∣αβn′ ∣ γ
替换为:
A → α A ′ ∣ γ A → β 1 ′ ∣ β 2 ′ ∣ β 3 ′ ∣ . . . ∣ β n ′ A →αA' | γ \\ A →β_1' \ \ | \ β_2' \ \ | \ β_3' \ \ | \ ... \ | \ β_n' A→αA′∣γA→β1′ ∣ β2′ ∣ β3′ ∣ ... ∣ βn′
文法G,消除左递归:
A → A α ∣ β A →Aα \ \ | \ β A→Aα ∣ β
替换为:
A → β A ′ A ′ → α A ′ ∣ ε A →βA' \\ A'→αA' \ | \ ε A→βA′A′→αA′ ∣ ε
正文
举例文法G:
E − > E + T ∣ E − T ∣ ε T − > T ∗ F ∣ T / F ∣ F F − > ( E ) ∣ i d E -> E + T \ | \ E - T \ | \ ε \\ T -> T * F \ | \ T \ / \ F \ | \ F \\ F -> ( \ E \ ) \ | \ id E−>E+T ∣ E−T ∣ εT−>T∗F ∣ T / F ∣ FF−>( E ) ∣ id
一、 提取左公因子
1. 归类
对一条产生式集合(ProductionSet)进行归类,存在相同左公因子的归为一类:
对于产生式头部 E E E,则有:
{ E + T , E − T } { ε } \{ \ E + T \ , \ E - T \ \} \\ \{ \ ε \ \} { E+T , E−T }{ ε }
对于产生式头部 T T T,则有:
{ T ∗ F , T / F } { F } \{ \ T * F \ , \ T \ / \ F \ \} \\ \{ \ F \ \} { T∗F , T / F }{ F }
2. 提取
对归类的每个类别进行分解提取左公因子; 注意,提取时是取当前类别所有项的最长公因子:
假设存在类别E,对其提取左公因子:
{ E + T , E − T } \{ \ E + T \ , \ E - T \ \} { E+T , E−T }
左公因子是: E E E
假设存在类别S,对其提取左公因子:
{ S + V , S + ( V ) , S − V } \{ \ S + V \ , \ S + ( \ V \ ) \ , \ S - V \ \} { S+V , S+( V ) , S−V }
左公因子是: S S S
有人可能认为类别S的左公因子是 S + S \ + S + (笑,刚开始做的时候我就是这样认为的…),如果变成 S + S + S+,则 S − V S - V S−V不能正确提取左公因子。
想想为什么要提取左公因子?
当选择产生式时,可能会面临不知道选哪个,提取左公因子的目的是为了将选择这个问题推迟到下一步,获取到足够的信息做出正确的选择。
那么,对于类别S,提取左公因子 S S S就算完结了吗?
S → { S S ′ } S ′ → { + V , + ( V ) , − V } S →\{ \ S \ S' \ \} \\ S' → \{ \ + V \ , \ + ( \ V \ ) \ , \ - V \ \} S→{ S S′ }S′→{ +V , +( V ) , −V }
S ′ S' S′依然不能做出正确的选择,所以还需要对 S ‘ S‘ S‘进行一次归类提取才可。
提取左公因子就这两步就可以了,剩下就是一些零碎的判断了,比如一个类别只有一项肯定是不能提取左公因子的,还有其他等等。关于提取的算法想了两个,但是好像复杂度都比较高,详细的步骤想法有写在注释中(CFG/extract()函数的注释) ; 如果有更好的算法欢迎交流~
我还是喜欢匹配current个子项的那个想法,因为结构类似与这样:
提取左公因子就像找到这样一个结构的最靠近开始的那个节点。
二、消除左递归
举例文法G:
E → E + T ∣ E − T ∣ F T → T ∗ F ∣ T / F ∣ F F → ( E ) ∣ D s ∣ E D s → D D s ∣ D D → 0 ∣ 1 ∣ 2 ∣ 3 ∣ 4 ∣ 5 ∣ 6 ∣ 7 ∣ 8 ∣ 9 E→ E + T \ | \ E - T \ | \ F \\ T→ T * F \ | \ T \ / \ F \ | \ F \\ F → ( \ E \ ) \ | \ Ds \ | \ E \\ Ds→ D \ Ds \ | \ D \\ D → 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 E→E+T ∣ E−T ∣ FT→T∗F ∣ T / F ∣ FF→( E ) ∣ Ds ∣ EDs→D Ds ∣ DD→0∣1∣2∣3∣4∣5∣6∣7∣8∣9
这个没什么好说的,龙书有详细的算法原理,通俗易懂(还是读了很多遍的)。
先看算法:
按照某个顺序将非终结符符号排序为 A 1 , A 2 , A 3 . . . . A n A_1,A_2,A_3.... \ A_n A1,A2,A3.... An.
f o r for for (从 1 1 1到 n n n的每个 i i i) {
f o r for for(从 1 1 1到 i − 1 i - 1 i−1的每个 j j j) {
将每个形如 A i → A j γ A_i \ → A_jγ Ai →Ajγ的产生式替换为产生式组 A i → δ 1 γ ∣ δ 2 γ ∣ δ 3 γ . . . ∣ δ k γ A_i \ → δ_1γ \ | \ δ_2γ \ | \ δ_3γ \ ... \ | \ δ_kγ Ai →δ1γ ∣ δ2γ ∣ δ3γ ... ∣ δkγ
其中 A i → δ 1 ∣ δ 2 ∣ δ 3 . . . ∣ δ k A_i \ → δ_1 \ | \ δ_2 \ | \ δ_3 \ ... \ | \ δ_k Ai →δ1 ∣ δ2 ∣ δ3 ... ∣ δk是所有的 A j A_j Aj产生式
}
消除 A i A_i Ai产生式之间的立即左递归;
}
当 i = 1 i = 1 i=1时(第一次迭代时),不进入内层循环,然后消除 A i A_i Ai的所有立即左递归;
因此余下的所有形如 A 1 → A l α A_1 \ → A_lα A1 →Alα的产生式都满足 l > 1 l > 1 l>1 ;
例如文法 G G G: E → E + T ∣ E − T ∣ F E→ E + T \ | \ E - T \ | \ F E→E+T ∣ E−T ∣ F 前两项消除立即左递归,余下的 E → F E→ F E→F 满足 F F F的索引比 E E E大 ;
当外层循环 i − 1 i - 1 i−1次迭代之后,所有非终结符号 A k ( k < i ) A_k(k< i) Ak(k<i)都被处理过 ; 任何产生式 A 1 k → A l α A_1k \ → A_lα A1k →Alα的产生式都满足 l > k l > k l>k ;
结果,在第 i i i次迭代时,内层循环提升所有形如 A i → A m α A_i → A_mα Ai→Amα的产生式中的 m m m的下界,直到 m ≥ i m≥i m≥i成立; 然后消除 A i A_i Ai产生式中的立即左递归,保证 m > i m>i m>i成立。
例如文法 G G G:当执行 F → ( E ) ∣ D s ∣ E F → ( \ E \ ) \ | \ Ds \ | \ E F→( E ) ∣ Ds ∣ E 时,比 F F F索引小的(例如 E E E)检查替换 E E E后是否会产生左递归,如果是则提升 E E E,提升后为 F E ’ F E’ FE’, 此时下界成立。
代码也很简单,检查能否左递归,如果能则替换产生式,然后执行一些零碎的操作,每一步的注释也比较详细。对于上面的算法我也只是理解的七七八八,如果有问题请帮忙指出,谢谢~
标签
语法分析 提取左公因子和消除左递归 JAVA实现