什么是消除直接左递归 - 编译原理解析
引言
在编译原理的语法分析中,文法(Grammar)是定义编程语言结构的基础。然而,某些文法形式会给解析器带来麻烦,尤其是左递归(Left Recursion)。本文将聚焦于"消除直接左递归",讲解它的定义、为什么需要消除,以及如何实现这一过程。
什么是左递归?
左递归是指文法中某个非终结符的产生式直接或间接地以自身作为第一个符号。例如:
css
复制代码
A -> A α | β
A 是一个非终结符。
α 和 β 是符号串(可以包含终结符和非终结符)。
A -> A α 是一个左递归产生式,因为推导时 A 立即又回到了 A。
直接左递归 vs 间接左递归
直接左递归 :非终结符直接以自身开头,如 A -> A α | β。
间接左递归 :通过其他非终结符间接回到自身,如:
css
复制代码
A -> B α
B -> A β
本文主要讨论直接左递归的消除,间接左递归的处理稍复杂,通常需要额外的步骤。
为什么需要消除直接左递归?
在语法分析中,尤其是自上而下分析(Top-Down Parsing)(如递归下降分析或 LL 分析),直接左递归会导致解析器陷入无限循环。例如:
r
复制代码
E -> E + T | T
解析 E 时:
选择 E -> E + T。
再次推导 E,又选择 E -> E + T。
无限重复,无法终止。
这种无限递归会使解析器无法正常工作,因此需要消除直接左递归,以确保解析器能够从左到右逐步处理输入。
如何消除直接左递归?
消除直接左递归的基本方法是将左递归文法改写为等价的非左递归文法。假设文法形式为:
css
复制代码
A -> A α1 | A α2 | ... | A αn | β1 | β2 | ... | βm
其中:
αi 是以 A 开头的部分(左递归部分)。
βj 是不以 A 开头的部分(非左递归部分)。
改写步骤
引入一个新的非终结符 A'(读作"A撇"),用于处理递归部分。
将原产生式拆分为两部分:
A -> β1 A' | β2 A' | ... | βm A'(以非左递归部分开头,后面接 A')。
A' -> α1 A' | α2 A' | ... | αn A' | ε(处理所有递归部分,ε 表示空串,作为终止条件)。
示例
考虑文法:
r
复制代码
E -> E + T | T
E 是非终结符。
E + T 是左递归部分(α = + T)。
T 是非左递归部分(β = T)。
改写过程
引入新非终结符 E'。
改写为:
r
复制代码
E -> T E'
E' -> + T E' | ε
解释
E -> T E':表达式以一个基本项 T 开始,后面接 E' 处理可能的加法。
E' -> + T E' | ε:E' 表示"表达式尾部",可以是 + T 加上更多的尾部(递归),也可以是空串(结束)。
对于输入 T + T:
E -> T E',匹配第一个 T。
E' -> + T E',匹配 + T。
E' -> ε,结束。
这样,解析器从左到右逐步推导,不会出现无限递归。
更复杂的例子
文法:
css
复制代码
A -> A α | A β | γ | δ
左递归部分:A α 和 A β(α 和 β 是不同的尾部)。
非左递归部分:γ 和 δ。
改写为:
css
复制代码
A -> γ A' | δ A'
A' -> α A' | β A' | ε
例如:
css
复制代码
S -> S a | S b | c | d
改写为:
bash
复制代码
S -> c S' | d S'
S' -> a S' | b S' | ε
输入 c a b:
S -> c S',匹配 c。
S' -> a S',匹配 a。
S' -> b S',匹配 b。
S' -> ε,结束。
消除直接左递归的意义
解析器兼容性:使文法适用于自上而下分析,避免无限递归。
等价性:改写后的文法生成的语言与原语言完全相同。
清晰性:将递归部分分离,便于理解和实现。
注意事项
消除直接左递归只适用于自上而下分析,自底向上分析(如 LR 分析)通常能直接处理左递归。
如果文法中存在间接左递归,需先将其转化为直接左递归,再进行消除。
总结
消除直接左递归是编译原理中优化文法的重要技术,通过引入新非终结符并重写产生式,可以将左递归文法转化为等价的非左递归形式。这一过程解决了自上而下解析器的无限递归问题,使语法分析更加高效和可行。如果您对编译器设计感兴趣,不妨尝试手动改写一个左递归文法,体验这一过程的奥妙!