文法中的递归以及消除方法

在介绍递归文法之前,首先介绍一下递归下降分析器及其原理,然后分析右递归是如何处理的,再来分析左递归和间接左递归。

递归下降分析器

自顶向下语法分析的目的是为输入串寻找最左推导,或者说,从根节点(文法开始符号)开始,自上而下,从左到右地为输入字符串建立一棵分析树,并以预先确定的顺序创建分析树的节点。这种自顶向下分析的一般形式,称之为递归下降分析法。“下降”表示自顶向下,“递归”表示可能会调用自身。

在递归下降分析器中,最简单的一种,就是 LL(1) 递归下降分析器。我们尝试来构造一个简单的满足 LL(1) 的递归下降语法分析器。语法结构如下

stat : assign_stat
	 | ifstat
	 | return_stat
	 ;
assign_stat : ID '=' expr ;
ifstat : IF expr THEN stat ;
return_stat : RETURN expr ;
expr : ID '<' NUM
	 | NUM
	 ;

当我们解析 if x < 0 then x = 0 这句语句时,会构造成如图 4-1 所示的解析树

文法中的递归以及消除方法_第1张图片
(图 4-1)

解析树里面含有语句的所有语法结构的信息。而解析就是将线性的词法单元序列组成带有结构的解析树。

语法解析器能检查句子的结构是否符合语法规范(语言实际上就是合法句子的集合)。为了验证句子是否合法,解析器必须识别句子的解析树。不过解析器实际上并不需要构造出形式上的树型结构,只要识别出各种句子结构和相关的词法单元即可。解析器不必构造出具体的解析树,只要为解析树中的指定子结构编写专用的函数,就能从解析函数的调用序列中隐式的得到解析树的信息。比如函数 f(),在匹配其子节点时会调用相应的函数,而需要匹配词法单元时会调用 match() 的辅助函数。顺着这条思路,可以为 return x+1 这条语句编写如下的识别函数

void stat() { returnstat(); }
void returnstat() { match("return"); expr();}
void expr() { match("x"); match("+"); match("1"); }

match() 函数将输入流里的词法单元与传入它的参数进行比较,然后将输入指针往前移动。但是对于 stat 语法分支,情况就会变得复杂,因为需要考虑到三种不同的情况,

void stat() {
	if (超前扫描词法单元是 return) returnstat();
	else if (超前扫描词法单元是 ID) assignstat();
	else if (超前扫描词法单元是 IF) ifstat();
	else 错误处理
}

这种自顶向下的语法解析器,从解析树的顶部开始,一直向下处理,直到叶子节点。

LL(1) 算法

LL(1)语法解析器,是一种相对来说最简单的自顶向下的语法解析器,两个 L 都表示 left-to-right,第一个 L 表示解析器按照从左到右的顺序解析输入内容,第二个 L 表示下降解析时也是按照从左到右的顺序遍历子节点。1 代表决定语法解析器的每一步动作时向前扫描一个词法单元。

正如上面构造出的简单的递归下降分析器,在 stat 中,如果是 ifstat 子句,我们会调用 ifstat() 函数,定义如下

void ifstat() {
	match("if");
	expr();
	match("then");
	stat();
}

在 ifstat 中,匹配到 then 后,后面还是一个 stat 子句,调用 stat 函数进行匹配。这里就是一个右递归。在 ifstat 匹配完成后,调用 stat 匹配 then 后面的部分。递归下降分析器通过调用自身,是可以处理右递归的。

左递归

对于形如

expr -> expr + term

这样的产生式,右部的最左符号与产生式左部的非终结符相同,这样的产生式即为左递归产生式。假定 expr 对应的过程要使用这个产生式,因为右部是由 expr 开始的,expr 过程会被递归调用,导致无限循环。只有右部终结符与超前扫描符号匹配时,超前扫描符号才会改变。这个产生式中,右部是以非终结符 expr 开始的,输入符号在递归调用期间没有机会改变,所以导致无限循环。

左递归如何消除

如果 文法 1 具有一个非终结符 A 使得对某个字符串 α \alpha α 存在推导
A    ⟹    A α (文法1) A \implies A\alpha \tag {文法1} AAα(1)
则称 文法1 是左递归的。

考虑下面 文法2
A    ⟹    B | a | C B D (1) A \implies \text{B | a | C B D} \tag 1 AB | a | C B D(1)
B    ⟹    C | b (2) B \implies \text{C | b} \tag 2 BC | b(2)
C    ⟹    A | c (3) C \implies \text{A | c} \tag 3 CA | c(3)
D    ⟹    d (4) D \implies d \tag 4 Dd(4)
由该文法能产生诸如 a, cbd 等的串。能够发现,该文法不是直接左递归的,因为并没有直接的类似于文法1一样的产生式。但是,文法 G 确实是左递归的,因为我们可以通过推导,获得
A    ⟹    A | c | b | a | C B D A \implies \text{A | c | b | a | C B D} AA | c | b | a | C B D
这就是左递归文法了。再来分析一下这个文法,计算一下文法的 FIRST 和 FOLLOW 集合。

在计算 FIRST 和 FOLLOW 集合之前,首先介绍一下这两个集合。

如果 α \alpha α 是任意的文法符号串,则我们定义 F I R S T ( α ) FIRST(\alpha) FIRST(α) 是从 α \alpha α 推导出的串的开始符号的终结符集合,即
F I R S T ( α ) = { a ∣ α    ⟹    a ⋯ , a 是 终 结 符 } FIRST(\alpha) = \{a | \alpha \implies a \cdots,a 是终结符 \} FIRST(α)={aαaa}
如果
α    ⟹    ϵ \alpha \implies \epsilon αϵ
ϵ \epsilon ϵ 也属于 F I R S T ( α ) FIRST(\alpha) FIRST(α) 。运用如下规则,来计算文法符号 X 的 FIRST(X) 集合,直到没有终结符或者 ϵ \epsilon ϵ 可加到某个 FIRST 集合为止

  • 如果 X 是终结符,则 FIRST(X) 是 {X}
  • 如果 X    ⟹    ϵ X \implies \epsilon Xϵ 是一个产生式,则将 ϵ \epsilon ϵ 加入到 FIRST(X) 中
  • 如果 X 是非终结符,且 $ X \implies Y_1Y_2Y_3…Y_n$ 是一个产生式,则
    • F I R S T ( Y i ) FIRST(Y_i) FIRST(Yi) 中所有符号在 FIRST(X) 中
    • 若对于某个 i,a 属于 F I R S T ( Y i ) FIRST(Y_i) FIRST(Yi) ϵ \epsilon ϵ 属于 F I R S T ( Y 1 ) FIRST(Y_1) FIRST(Y1) ,……。 F I R S T ( Y i − 1 ) FIRST(Y_{i-1}) FIRST(Yi1) ,即 Y 1 . . . Y i − 1    ⟹    ϵ Y_1...Y{i-1} \implies \epsilon Y1...Yi1ϵ ,则将 a 加入到 FIRST(X) 中
    • 若对于所有的 j=1, 2, …, k, ϵ \epsilon ϵ F I R S T ( Y i ) FIRST(Y_i) FIRST(Yi) 中,则将 ϵ \epsilon ϵ 加入到 FIRST(X) 中

FOLLOW(A) 是计算所有非终结符 A 的后继符号的集合。可以应用如下规则,直到每个 FOLLOW(A) 集合都不能在加入任何符号或者 $ 为止

  • 将 $ 加入到 FOLLOW(S) 中,其中 S 是开始符号,$ 是输入串的结束符
  • 如果存在产生式 A    ⟹    α B β A \implies \alpha B \beta AαBβ ,则将 F I R S T ( β ) FIRST(\beta) FIRST(β) 中除了 ϵ \epsilon ϵ 以外的符号都加入到 FOLLOW(B) 中
  • 如果存在产生式 A    ⟹    α B A \implies \alpha B AαB ,或者 A    ⟹    α B β A \implies \alpha B \beta AαBβ ,其中 F I R S T ( β ) FIRST(\beta) FIRST(β) 中包含 ϵ \epsilon ϵ ,即 β    ⟹    ϵ \beta \implies \epsilon βϵ ,则将 FOLLOW(A) 中的所有符号都加入到 FOLLOW(B) 中

FIRST 和 FOLLOW 集合分别表示了从文法推导出的串的开始符号的终结符集合和后继终结符号的集合,在语法解析时,通过超前扫描的方式,获取到的token,与产生式的 FIRST 和 FOLLOW 集合比较,能够判断出语法适配到的产生式或者分支,做到预测分析的能力。

我们再来看下文法2,
F I R S T ( A ) = F I R S T ( B ) ⋃ F I R S T ( C B D ) ⋃ a F I R S T ( B ) = F I R S T ( C ) ⋃ b F I R S T ( C ) = F I R S T ( A ) ⋃ c \begin{array}{ccc} FIRST(A) = FIRST(B) \bigcup FIRST(C B D) \bigcup \text{{a}} \\ FIRST(B) = FIRST(C) \bigcup \text{{b}} \\ FIRST(C) = FIRST(A) \bigcup \text{{c}} \\ \end{array} FIRST(A)=FIRST(B)FIRST(CBD)aFIRST(B)=FIRST(C)bFIRST(C)=FIRST(A)c
同时, F I R S T ( C B D ) = F I R S T ( C ) FIRST(CBD)= FIRST(C) FIRSTCBD=FIRSTC,由 (1) 和 (3) 可得
F I R S T ( A ) = F I R S T ( B ) ⋃ F I R S T ( C ) ⋃ a    ⟹    F I R S T ( A ) = F I R S T ( B ) ⋃ F I R S T ( A ) ⋃ c ⋃ a    ⟹    F I R S T ( A ) = F I R S T ( C ) ⋃ b ⋃ a, c    ⟹    F I R S T ( A ) = a, b, c \begin{array}{cccc} FIRST(A)=FIRST(B) \bigcup FIRST(C) \bigcup \text{{a}} \\ \implies FIRST(A) = FIRST(B) \bigcup FIRST(A) \bigcup \text{{c}} \bigcup \text{{a}} \\ \implies FIRST(A) = FIRST(C) \bigcup \text{{b}} \bigcup \text {{a, c}} \\ \implies FIRST(A) = \text{{a, b, c}} \end{array} FIRST(A)=FIRST(B)FIRST(C)aFIRST(A)=FIRST(B)FIRST(A)caFIRST(A)=FIRST(C)ba, cFIRST(A)=a, b, c
同理,可计算出
F I R S T ( C ) = F I R S T ( A ) ⋃ c = a, b, c F I R S T ( B ) = F I R S T ( C ) ⋃ b = a, b, c F I R S T ( D ) = d FIRST(C) = FIRST(A) \bigcup \text{{c}} = \text{{a, b, c}} \\ FIRST(B) = FIRST(C) \bigcup \text{{b}} = \text{{a, b, c}} \\ FIRST(D) = \text{{d}} FIRST(C)=FIRST(A)c=a, b, cFIRST(B)=FIRST(C)b=a, b, cFIRST(D)=d
计算 FOLLOW(A) 集合,首先需要找出非终结符 A 出现在右边的所有产生式,在文法2 的产生式 (3) 中,A 出现在产生式右边,同时也是在结束位置,所以 FOLLOW(A) 是包含 FOLLOW( C )。

再来看非终结符 C,在产生式 (1) 中, A    ⟹    C B D A \implies C B D ACBD,右边 C 的 右边是 B,那么 FOLLOW( C ) 是包含 FIRST(B) 的,在产生式 (2) 中,右边出现 C 同时也是产生式的结束位置,所以 FOLLOW( C ) 是包含 FOLLOW(B) 的。

再看 B,在产生式 (1) 中,B 右边出现的是 D,那么$ FOLLOW(B)$ 包含 F I R S T ( D ) = d FIRST(D)=\text{{d}} FIRST(D)=d ,同时 B 也是产生式的结束位置,那么 FOLLOW(B) 包含 FOLLOW(A),由以上所得
F O L L O W ( C ) = F I R S T ( B ) ⋃ F O L L O W ( B ) = a, b, c, d F O L L O W ( A ) = F O L L O W ( C ) = F O L L O W ( B ) = a, b, c, d FOLLOW(C) = FIRST(B) \bigcup FOLLOW(B) = \text{{a, b, c, d}} \\ FOLLOW(A) = FOLLOW(C) = FOLLOW(B) = \text{{a, b, c, d}} FOLLOW(C)=FIRST(B)FOLLOW(B)=a, b, c, dFOLLOW(A)=FOLLOW(C)=FOLLOW(B)=a, b, c, d
在产生式 (1) 中,D 出现在产生式结束位置,所以
F O L L O W ( D ) = F O L L O W ( A ) = a, b, c, d FOLLOW(D) = FOLLOW(A) = \text{{a, b, c, d}} FOLLOW(D)=FOLLOW(A)=a, b, c, d
根据 FIRST 集合和 FOLLOW 集合,就可以进行预测分析,但是文法2,由于左递归的性质,当超前扫描是字符 a 时,并不能直接推测出匹配的产生式是 1-3 中的哪一个,而在递归下降语法解析器中,左递归会导致无限循环,这就需要使用到左递归消除。

左递归消除方法

对于文法2中的产生式 (1) 我们要将产生式变换成直接左递归的形式,如下
A    ⟹    B ∣ a ∣ C B D A \implies B | a | \text{C B D} \\ ABaC B D
将产生式 (2) 放进去替换 B
A    ⟹    C ∣ b ∣ a C B D A \implies C | b | a \text{C B D} ACbaC B D
将产生式 (3) 放进去替换 B
A    ⟹    A ∣ c ∣ b ∣ a ∣ A B D ∣ c B D    ⟹    A ∣ A B D ∣ c | b | a | c B D A \implies A | c | b | a | \text{A B D} | \text{c B D} \implies A | \text{A B D} | \text{c | b | a | c B D} AAcbaA B Dc B DAA B Dc | b | a | c B D
产生式右部中出现的 A,对于 A    ⟹    A A \implies A AA 没什么实际意义,可以直接去掉,产生式也就变成了
A    ⟹    A B D ∣ c | b | a | c B D A \implies \text{A B D} | \text{c | b | a | c B D} AA B Dc | b | a | c B D
这很明显是一个直接左递归的产生式,能够产生以 c 或者 b 或者 a 或者 cBD 交替出现的,以BDBDBD……结束的串。我们将 A 进行变形,按照如下方式进行变换
A    ⟹    ( c | b | a | c B D ) A ′ A ′    ⟹    ϵ ∣ B D A ′ A \implies (\text{c | b | a | c B D})A^{\prime} \\ A^{\prime} \implies \epsilon | BDA^{\prime} A(c | b | a | c B D)AAϵBDA
这样,就将直接左递归变成了右递归,而右递归,递归下降分析器是可以处理的,这样,语法分析器就能正常的处理文法规则了。

将上面左递归消除的方法一般化,对形如
P    ⟹    P α ∣ β ,    其 中 β 不 以 P 开 头 (文法3) P \implies P \alpha | \beta ,\ \ 其中\beta不以P开头 \tag {文法3} PPαβ,  βP(3)
的文法,按照如下方式进行左递归消除(如果不是直接左递归的方式,先转换成直接左递归)
P    ⟹    β P ′ P ′    ⟹    α P ′ ∣ ϵ P \implies \beta P^{\prime} \\ P^{\prime} \implies \alpha P^{\prime} | \epsilon PβPPαPϵ
可以证明一下,对于
P    ⟹    P α ∣ β P \implies P \alpha | \beta PPαβ
产生式能够产生 β α α α … \beta \alpha \alpha \alpha \ldots βααα 的串,对于消除左递归之后的文法,做如下变换
P ′    ⟹    α α α … P    ⟹    β P ′    ⟹    β α α α … P^{\prime} \implies \alpha \alpha \alpha \ldots \\ P \implies \beta P^{\prime} \implies \beta \alpha \alpha \alpha \ldots PαααPβPβααα
两个产生式产生的串是等价的,也就证明了消除后的右递归的文法与之前的直接左递归的文法是等价的。

对于更一般的情况,形如
P    ⟹    P ( α 1 ∣ α 2 ∣ … ∣ α n ) ∣ ( β 1 ∣ β 2 ∣ … ∣ β n ) P \implies P(\alpha_1 | \alpha_2 | \ldots | \alpha_n) | (\beta_1 | \beta_2 | \ldots | \beta_n) PP(α1α2αn)(β1β2βn)
这样的文法规则,如果不是上面这样的表示,可以通过提取公因子的方式,变换成上面所示的产生式的形式,然后,还是按照上面文法 3 所示的方法进行递归消除,只不过此处,我们可以将 α 1 ∣ α 2 ∣ … ∣ α n \alpha_1 | \alpha_2 | \ldots | \alpha_n α1α2αn 记为 α \alpha α,将 β 1 ∣ β 2 ∣ … ∣ β n \beta_1 | \beta_2 | \ldots | \beta_n β1β2βn 记为 β \beta β

间接左递归

indirectly left recursive grammar is that where x calls y which calls x.

通俗的讲,就是类似 A    ⟹    B , 和 B    ⟹    A A\implies B,和 B \implies A ABBA 这种文法规则,而 hidden left recursive grammar 是产生是右边允许为空集的情况,比如 A    ⟹    B A , 和 B    ⟹    ϵ A \implies BA,和 B \implies \epsilon ABABϵ

比如 文法 4
S    ⟹    Q c ∣ c (5) S \implies Q c | c \tag 5 SQcc(5)
Q    ⟹    R b ∣ b (6) Q \implies R b | b \tag 6 QRbb(6)
R    ⟹    S a ∣ a (7) R \implies S a | a \tag 7 RSaa(7)
该文法就是一个间接左递归的文法。利用前面所讲的递归下降语法解析器来说,文法 4 也会产生无限循环,解析器不能从递归嵌套的无限循环中解放出来。

对文法 4 进行替换变形,将 (7) 代入 (6)
Q    ⟹    S a b ∣ a b ∣ b (8) Q \implies S a b | a b | b \tag 8 QSababb(8)
将 (8) 代入 (5)
S    ⟹    S a b c ∣ a b c ∣ b c ∣ c (9) S \implies S a b c | a b c | b c | c \tag 9 SSabcabcbcc(9)
可见,通过替换变形,产生式 (5) 变成了产生式 (9),变成了一个直接左递归的产生式,在按照直接左递归的变化方法
S    ⟹    ( a b c | b c | c ) S ′ (10) S \implies (\text{a b c | b c | c}) S^{\prime} \tag {10} S(a b c | b c | c)S(10)
S ′    ⟹    ϵ ∣ a b c S ′ (11) S^{\prime} \implies \epsilon | a b c S^{\prime} \tag {11} SϵabcS(11)
此时,文法 S 可以直接由 S ′ S^{\prime} S 和 abc 推导,Q 和 R 已经不需要了,直接删除即可。那么,产生式 (10) 和 产生式 (11) 就是文法 4 由间接左递归消除后变成直接右递归的结果。

一般来说,左递归的消除,不管是左递归还是间接左递归,最终都是需要消解成右递归的方式,因为递归下降解析器最适合处理的就是右递归。面对间接左递归,先通过代换的方式变形成直接左递归,然后在将直接左递归消解成右递归。

消除全部左递归算法

消除全部左递归,要求文法中

  • 不含以 ϵ \epsilon ϵ 为右部的产生式
  • 不含回路

算法实现

  1. 把文法中所有的非终结符按任意顺序排列

P 1 , P 2 , ⋯   , P n P_1, P_2, \cdots, P_n P1,P2,,Pn

for i=1 To n; DO

BEGIN

​	for j=1 To i-1; DO

​把形如 P i → P j γ P_i \to P_j\gamma PiPjγ 的规则写成 P i → δ 1 γ ∣ δ 2 γ ∣ ⋯ ∣ δ k γ P_i \to \delta_1 \gamma | \delta_2 \gamma | \cdots | \delta_k \gamma Piδ1γδ2γδkγ ,其中 P j → δ 1 ∣ δ 2 ∣ ⋯ ∣ δ k P_j \to \delta_1 | \delta_2 | \cdots | \delta_k Pjδ1δ2δk , δ i \delta_i δi 就是关于 P i P_i Pi 的所有规则

​ 消除关于 P i P_i Pi 的所有直接左递归

END
  1. 将从开始符号出发永远无法到达的规则删除

也就是说,对每个非终结符号,用排在它前面的其他非终结符号的产生式表示出来(代入),并消除产生式中的直接左递归

reference

  1. 编程语言实现模式,第2章
  2. 编译原理龙书,第4章
  3. CS164-left recursion elimination example

待填坑

  1. antlr4 是如何解决左递归的

你可能感兴趣的:(antlr4,cookbook,antlr4,上下文无关文法)