本篇文章内的源码: 这里
当我们自顶向下的语法分析时,就需要采用最左推导方式。
而这个时候,如果产生式左部和产生式右部首字符一样(即A→Aα
),那么推导就可能陷入无限循环。
例如:
文法G
1. S -> Sa | b
推导
S => Sa => Saa => ... => Sa...a
因此对于:
- 含有
A→Aα
形式产生式的文法称为是直接左递归
。 - 如果文法中一个非终结符
A
,存在一步以上的推导,形成了A =>+ Aα
,称为间接左递归
。例如:
A→Bβ
和B -> Aβ
可以得到A => Bβ => Aββ
文法中不能包含这两种形式,不然最左推导就没办法进行。
那有人问了,如果产生式右部中间包含和产生式左部相同的字符,允不允许呢?
- 大多数情况下,是允许的,因为我们采用的是最左推导,只要这个产生式右部在这个非终结符之前有其他终结符(即
A→αA
),那么就可以确定推导时,用不用这个产生。- 如果产生式右部在这个非终结符之前是非终结符(即
A→BAβ
),那么就要看B
能不能推导出空串ε
了,如果可以,那么最终还是会推导出A =>+ Aβ
, 也属于间接左递归
。
一. 算法描述
1.1 消除直接左递归
例如:
文法G
1. S -> Sa | b
那么该如何改造呢?
其实我们仔细分析一个这组产生式,我们发现它可以进行如下推导:
S => b
或者
S => Sa
=> Saa
...
=> Sa...a
=> ba..a
你会惊奇的发现,它能推导出 b
和 (a)* (即由0
个a
或者无数个a
生成的文法符号串)。其实就可以改造成:
文法G
1. S -> bS'
2. S' -> aA' | ε
S'
就可以表示 (a)* ,因此S'
必须有空产生式S' -> ε
,否则就得不到0
个a
的空串。
因此消除直接左递归
算法的一般形式:
A -> Aα1 | Aα2 | ... | Aαn | β1 | β2 | ... | βm
(其中这里数字表示下标,而不是一个终结符, α 和 β都是文法符号串)
(αi != ε, 且 βi 不以A开头)
改造成
A -> β1A' | β2A' | ... | βmA'
A' -> α1A' | α2A' | ... | αnA' | ε
1.2 消除间接左递归
例如:
文法G:
1. S -> Aa | b
2. A -> c | Sd
间接左递归
S => Aa
=> Sda
=> Aada
=> Sdada
消除间接左递归的方法就是直接带入消除,即
文法G:
1. S -> Aa | b
2. A -> c | Aad | bd
即将能形成间接左递归产生式中的非终结符,替换成这个非终结符的产生式组。
因此消除间接左递归
算法的一般形式:
按照某个顺序将非终结符进行排序,为A1,A2,...,An
for (从1到n的每个i) {
for (从1到i-1的每个j) {
将每个形如Ai -> Ajβ的产生式,用Aj 的产生式组Aj -> α1 | α2 | ... | αk 替换;
得到产生式 Ai -> Ajβ 替换后的产生式组 Ai -> α1β | α2β | ... | αkβ。
}
}
这个算法看起来描述很多,其实理解起来很简单:
- 先将这些非终结符进行排序,然后开始遍历这些非终结符的产生式组
- 判断当前非终结符
Ai
的每一个产生式右部首字符,是不是前面已经遍历过的非终结符Aj
。这个就是为什么需要对非终结符进行排序
- 如果是那么就会形成间接左递归,就要用前面非终结符
Aj
的产生式组当前非终结符Ai
这个产生式中的非终结符Aj
,这样当前这个产生式变成了多个产生式了。
二. 算法实现
2.1 Symbol
类
/**
* @author by xinhao 2021/8/13
* 表示文法中字母表中一个符号,包括终结符和非终结符
*/
public class Symbol implements Comparable<Symbol> {
public static final String EPSILON = "ε";
// 文法结束符号
public static final Symbol END = new Symbol("$", true);
// 表示符号的值, 这里用 String 表示,而不是 char,
// 是因为有些符号我们不好用一个 char 表示。 比如 A 对应的 A'
private final String label;
private final boolean isTerminator;
// 外部不能创建
Symbol(String label, boolean isTerminator) {
this.label = label;
this.isTerminator = isTerminator;
}
public String getLabel() {
return label;
}
public boolean isTerminator() {
return isTerminator;
}
// 这里只判断 label 值
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Symbol symbol = (Symbol) o;
return Objects.equals(label, symbol.label);
}
@Override
public int hashCode() {
return Objects.hash(label);
}
@Override
public String toString() {
return this.label;
}
@Override
public int compareTo(Symbol o) {
return label.compareTo(o.label);
}
}
用来表示文法中字母表中一个符号,包括终结符和非终结符
2.2 Alphabet
类
/**
* @author by xinhao 2021/8/13
* 字母表, 这个类不用实例化。用来记录字母表中所有的符号
*/
public abstract class Alphabet {
/**
* 字母表中所有的符号
*/
public static final Map<String, Symbol> ALL_SYMBOL = new HashMap<>();
// 初始化
static {
for (int index = 0; index <= 9; index++) {
ALL_SYMBOL.put("" + index, new Symbol("" + index, true));
}
// a-z
for (char ch = 97; ch <= 122; ch++) {
ALL_SYMBOL.put(String.valueOf(ch), new Symbol(String.valueOf(ch), true));
}
// A - Z
for (char ch = 65; ch <= 90; ch++) {
ALL_SYMBOL.put(String.valueOf(ch), new Symbol(String.valueOf(ch), false));
}
ALL_SYMBOL.put("+", new Symbol("+", true));
ALL_SYMBOL.put("-", new Symbol("-", true));
ALL_SYMBOL.put("*", new Symbol("*", true));
ALL_SYMBOL.put("(", new Symbol("(", true));
ALL_SYMBOL.put(")", new Symbol(")", true));
}
public static Symbol addSymbol(char ch) {
return addSymbol(ch, true);
}
public static Symbol addSymbol(char ch, boolean isTerminator) {
return addSymbol(String.valueOf(ch), isTerminator);
}
public static Symbol addSymbol(String label) {
return addSymbol(label, true);
}
public static Symbol addSymbol(String label, boolean isTerminator) {
Symbol symbol = new Symbol(label, isTerminator);
ALL_SYMBOL.put(label, symbol);
return symbol;
}
public static Symbol getSymbol(char ch) {
return getSymbol(String.valueOf(ch));
}
public static Symbol getSymbol(String label) {
return ALL_SYMBOL.get(label);
}
}
表示字母表,用来记录字母表中所有的符号。
2.3 Production
类
/**
* @author by xinhao 2021/8/13
* 文法中的产生式
*/
public class Production {
public static final List<Symbol> EMPTY = new ArrayList<>(0);
/** 产生式左边,而且必须是非终结符 */
private final Symbol left;
/** 这里的 List<Symbol> 希望是不可变的,你们可以自己引入 ImmutableList */
private final List<Symbol> right;
/** 表示这个产生式不是左递归产生式 */
private final boolean isLeftEliminate;
....
}
用来表示文法中的一个产生式,例如 A -> bB
。
2.4 Grammar
类
public class Grammar {
// 开始符号
private final Symbol start;
/**
* 这里为什么用 map 类型?
* S -> aA | bB
* A -> aaaa
* A -> abab | ε
* B -> bbb
* B -> bababa
* B -> ε
* 所以对于同一个左部 Symbol (A) 其实是有多个产生式的,就是产生式租
* 使用 LinkedHashMap ,因为产生式是要有记录顺序的
*/
private final LinkedHashMap<Symbol, List<Production>> productions;
...
}
表示一个文法,包含文法开始符号和产生式组,终结符号集合非终结符号集没有包含在这个里面。
2.5 消除直接左递归算法
public class Production {
......
public boolean isLeftEliminate() {
if (right.isEmpty()) {
return false;
}
// 产生式左部 与 产生式第一个字母相等,表示它是一个直接递归的产生式。
return left.equals(right.get(0));
}
}
/**
* 消除直接左递归
* 就是将这种格式 P -> Pa|Pb|Pc|Pd|e|f|g|h
* 转成:
* P -> eP'|fP'|gP'|hP'
* p'-> aP'|bP'|cP'|dP'|ε
*
*/
public static Grammar eliminateDeepLeftRecursion(Grammar grammar) {
List<Production> productions = new ArrayList<>();
for (Map.Entry<Symbol, List<Production>> entry : grammar.getProductions().entrySet()) {
// 不包含直接左递归的 产生式集合
List<Production> noLeftEliminateProductions = new ArrayList<>();
// 包含直接左递归的 产生式集合; 即 A -> Aa 这种形式
List<Production> leftEliminateProductions = new ArrayList<>();
for (Production production : entry.getValue()) {
if (production.isLeftEliminate()) {
leftEliminateProductions.add(production);
} else {
noLeftEliminateProductions.add(production);
}
}
// 表示这个 entry.getKey() 对应的产生式组 List<Production> 没有直接左递归产生式
if (leftEliminateProductions.isEmpty()) {
// 不用做任何修改,直接将 noLeftEliminateProductions 添加到 productions
productions.addAll(noLeftEliminateProductions);
} else {
// 需要进行转换了, 即 A 变成 A'
Symbol newKey = Alphabet.addSymbol(entry.getKey().getLabel() + "'", false);
// 将原来的 P -> e|f|g|h
// 变成 P -> eP'|fP'|gP'|hP'
for (Production production : noLeftEliminateProductions) {
// 创建新的产生式右部 e 变成 eP' 或者 f 变成 fP'
List<Symbol> newRight = production.addSymbolToRight(newKey);
// 这个产生式的 left 还是 原来的
productions.add(Production.create(entry.getKey(), newRight));
}
// 将原来的 P -> Pa|Pb|Pc|Pd
// 变成 P -> aP'|bP'|cP'|dP'|ε
for (Production production : leftEliminateProductions) {
// 创建新的产生式右部 Pa 变成 aP' 或者 Pd 变成 dP'
List<Symbol> newRight = production.addSymbolToRightAndRemoveFirst(newKey);
// 这个产生式的左部,就必须是新的 key 了
productions.add(Production.create(newKey, newRight));
}
// 最后对这个新左部,必须添加 空产生式
productions.add(Production.createEmpty(newKey));
}
}
return Grammar.create(grammar.getStart(), productions);
}
其实算法很简单,就是遍历文法中,每一个非终结符对应的产生式组,查看是否包含直接左递归的形式,如果包含就将它变成两个产生式组:
P -> Pa|Pb|Pc|Pd|e|f|g|h
变成:
P -> eP'|fP'|gP'|hP'
p'-> aP'|bP'|cP'|dP'|ε
2.6 消除间接左递归算法
/**
* 消除间接左递归
* 如同 S -> Aa|b A -> Ac|Sd
* 会产生 S ==> Aa ==> Sda
* 消除间接左递归变成
* S -> Aa|b
* A -> Ac|Aad|bd
*/
public static Grammar eliminateIndirectLeftRecursion(Grammar grammar) {
// 记录之前遍历的 产生式
LinkedHashMap<Symbol, List<Production>> preProductionsMap = new LinkedHashMap<>();
for (Map.Entry<Symbol, List<Production>> entry : grammar.getProductions().entrySet()) {
List<Production> entryProductions = new ArrayList<>();
for (Production production : entry.getValue()) {
// 得到产生式右部第一个字符,例如 A -> Sd 其中 S 就是第一个字符
Symbol symbol = production.getRightFirst();
// 这个产生式右部第一个字符之前出现过,例如 S -> Aa A -> Sd ,这个 S 就在之前出现过,
// 那么就产生了间接左递归,那么就需要做转换了。就使用 S 的产生式组,替换它在A的中产生式。
// symbol = null 没有关系,因为我们不会将 null 存入 preProductionsMap 集合中
if (preProductionsMap.containsKey(symbol)) {
// 将 symbol 添加到 leftEliminateSet 集合中,表示它是间接左递归
List<Production> eliminateList = preProductionsMap.get(symbol);
for (Production eliminateProduction : eliminateList) {
// 例如 S -> Aa|b A -> Ac|Sd,
// 这里 eliminateList 就是 [Aa, b], 要用它替换产生式[Sd],变成 [Aad, bd]
entryProductions.add(Production.create(production.getLeft(),
production.replaceRightFirst(eliminateProduction)));
}
} else {
// 不是间接左递归,那么就直接添加
entryProductions.add(production);
}
}
preProductionsMap.put(entry.getKey(), entryProductions);
}
return Grammar.create(grammar.getStart(), preProductionsMap);
}
算法其实也很简单,按照一定顺序,遍历文法中非终结符对应的产生式组,如果发现有产生式右部的首字符与之前遍历过的非终结符相同,说明有间接左递归现象,那么就要这个非终结符对应的产生式组替换这个非终结符。
但是注意这里判断是否有间接左递归,只是靠产生式右部的首字符与之前遍历过的非终结符相同;其实这个是不完整的。
- 例如有三个产生式
A -> B
B -> CA
C -> ε
,那么非终结符B
是可以推导出B => CA => A
的,所以这种情况也属于间接左递归。- 所以判断间接左递归,不止要看首字符,还要包括产生式右部包含前面出现的字符(即
B -> αA
),那么文法符号串α
的串首终结符集FIRST(α)
是否包含空串 ε。这个算法以后再介绍。
2.7 例子
public static void main(String[] args) {
Symbol start = Alphabet.getSymbol('S');
List<Production> productions = new ArrayList<>();
productions.addAll(Production.createList(start, "Aa", "b", "ε"));
productions.addAll(Production.createList(Alphabet.getSymbol('A'), "Ac", "Sd"));
Grammar grammar = Grammar.create(start, productions);
Grammar indirectGrammar = GrammarUtils.eliminateIndirectLeftRecursion(grammar);
Grammar deepGrammar = GrammarUtils.eliminateDeepLeftRecursion(indirectGrammar);
System.out.println(grammar);
System.out.println(indirectGrammar);
System.out.println(deepGrammar);
}
运行结果:
开始符号:S
产生式:
S->Aa|b|ε
A->Ac|Sd
开始符号:S
产生式:
S->Aa|b|ε
A->Ac|Aad|bd|d
开始符号:S
产生式:
S->Aa|b|ε
A->bdA'|dA'
A'->cA'|adA'|ε