LL1
前言
LL1是编译原理自上至下分析方法的一种,是其他分析方法的基础。本篇文章将通过一个例子附带代码讲述整个过程。读者一定要喜欢哦。
文法数据结构
const grammar = {
S: [["M", "H"], ["a"]],
H: [["L", "S", "o"], [EMPTY]],
K: [["d", "M", "L"], [EMPTY]],
L: [["e", "H", "f"]],
M: [["K"], ["b", "L", "M"]],
};
上述的数据结构表示的文法为
- S -> MH | a
- S -> LSo | ε
- S -> dML | ε
- L -> eHf
- M -> K | bLM
终结符和非终结符数据结构
const nonTermialSymbol = new Set(["S", "H", "K", "L", "M"]);
const terminalSymbol = new Set([EMPTY, "a", "b", "d" ,"e", "o", "$", "f"]);
上面将 ε 和 $ 归结于终结符的目的是为了后面算法简单。
消除二义性文法
对于多数的二义性文法,可以在建立LL1表的时候给予用户提示,为了消除二义性文法可能会引入多余的文法,这是不值得的,例如对于 if else
的就近匹配问题,我们在后期解决的时候可以选择合适的解决文法冲突。
消除左递归和提取左公因子
请读者自行消除左递归和提取左公因子后放入上述文法数据结构中,由于该步骤人工处理较为简单,可以自行完成。
First 集合
First(X) 集合表示可以从 X 退出所有串首终结符
-
若 X 是一个终结符,那么 First(X) = { X }
-
若存在表达式 X->Y1Y2Y3…Yn,将Y1的First集合加入First(X)中,若First(X)包含ε则迭代Y2,将Y2的First集合加入First(X) 中,循环迭代至结束。
-
若存在表达式 X -> ε,则将ε加入First(X)中。
算法优化:
可以看出,若视ε为终结符,1和3可以合并。
合并后,1和2也可以合并,只要单独判断是否为终结符就可以了。
代码:
const EMPTY = "ε"
const firstSet = {};
const processFirst = () => {
for (let nts of nonTermialSymbol) firstSet[nts] = new Set();
for (let nts of nonTermialSymbol) _processFirst(nts);
};
const _processFirst = (nts) => {
for (let grammarArr of grammar[nts]) {
const len = grammarArr.length;
for (let i = 0; i < len; i += 1) {
const ch = grammarArr[i];
if (terminalSymbol.has(ch)) {
firstSet[nts].add(ch);
break;
} else {
_processFirst(ch);
for (let v of firstSet[ch]) firstSet[nts].add(v);
if (!firstSet[ch].has(EMPTY)) break;
}
}
}
};
这里的话,有可以改进的地方,例如某个first集合算过后,就可以直接省略计算,即加一个标志位标志一下就可以了,但是由于First集合算法本身不太耗时,而且只是为了导出LL1分析表,拿到LL1分析表后,其实在之后的编译过程中不会重复用到,因此其实优化其实可有可无,感兴趣的读者可以自行加上。
Follow 集合
- 若S是开始符号,则将 $ 添加到 Follow (S) 中
- 若存在一个产生式 X -> Y1Y2Y3…Yn 则 Follow(Yn) 集合需要加上 Follow(X) 集合,若 First (Yn) 包含 ε 则,向左迭代,即 Follow(Yn-1) 集合需要加上 Follow(X) 集合,若 First (Yn-1) 包含 ε 则,向左迭代… 迭代至表达式结束。
- 若存在一个表达式 X -> ABCD 则 Follow(A) 需要加上 First(B) - ε,若First(B) 包含 ε,则Follow(A) 需要加上 First© - ε,向右迭代… 迭代至表达式结束。
从算法层面来看,这个follow集合的计算是比较复杂的,而且需要重复计算,直到先前的follow集合和后来的follow集合相等才停止。
const { cloneDeep, isEqual } = require("lodash");
const START = "S";
const END = "$";
const followSet = {};
let prevFollowSet = {};
const processFollow = () => {
for (let nts of nonTermialSymbol) followSet[nts] = new Set();
followSet[START].add(END);
while (true) {
let flag = false;
for (let nts of nonTermialSymbol) {
_processFollow(nts);
if (isEqual(prevFollowSet, followSet)) flag = true;
else flag = false;
prevFollowSet = cloneDeep(followSet);
}
if (flag) break;
}
};
const _processFollow = (_nts) => {
// 3 若存在一个表达式 X -> ABCD 则 Follow(A) 需要加上 First(B) - ε,若First(B) 包含 ε,则Follow(A) 需要加上 First(C) - ε,向右迭代... 迭代至表达式结束。
for (let nts of nonTermialSymbol) {
for (let grammarArr of grammar[nts]) {
const len = grammarArr.length;
let index = -1;
// 找到 B
for (let i = 0; i < len; i += 1) {
const ch = grammarArr[i];
if (ch === _nts) {
index = i;
break;
}
}
if (index === -1) break;
while (index + 1 < len) {
const ch = grammarArr[index + 1];
if (terminalSymbol.has(ch)) {
followSet[_nts].add(ch);
break;
} else {
const set = firstSet[ch];
for (let v of set) if (v !== EMPTY) followSet[_nts].add(v);
if (!set.has(EMPTY)) break;
index += 1;
}
}
}
}
// 2 若存在一个产生式 X -> Y1Y2Y3...Yn 则 Follow(Yn) 集合需要加上 Follow(X) 集合,若 First (Yn) 包含 ε 则,向左迭代,即 Follow(Yn-1) 集合需要加上 Follow(X) 集合,若 First (Yn-1) 包含 ε 则,向左迭代... 迭代至表达式结束。
const set = followSet[_nts];
for (let grammarArr of grammar[_nts]) {
const len = grammarArr.length;
for (let i = len - 1; i >= 0; i -= 1) {
const ch = grammarArr[i];
if (terminalSymbol.has(ch)) break;
const newSet = followSet[ch];
for (let v of set) {
newSet.add(v);
}
if (!firstSet[ch].has(EMPTY)) {
break;
}
}
}
};
由于需要深度克隆和判断集合的值是否相等,因此引入了 lodash
Select 集合
是创造 LL1 表的最后一步工作。为了计算Select集合,首先要先算文法表达式右侧的First集合。
即对 A -> BCD
- 计算 First(BCD)。则先计算First(B),若包含ε,计算First©,若仍包含ε,则继续往下迭代。直到遇到终结符或者遇到非εFirst集合。
- 若First(BCD) 包含 ε,则Select(A)= First(BCD) 和 Follow(A) 的并集合去掉 ε。若不包含 Select(A) = First(BCD)
const processSelect = () => {
for (let nts of nonTermialSymbol) {
for (let grammarArr of grammar[nts]) {
const set = new Set();
const len = grammarArr.length;
// 1. 计算 First(BCD)。则先计算First(B),若包含ε,计算First(C),若仍包含ε,则继续往下迭代。直到遇到终结符或者遇到非εFirst集合。
for (let i = 0; i < len; i++) {
const ch = grammarArr[i];
if (terminalSymbol.has(ch)) {
set.add(ch);
break;
}
for (let v of firstSet[ch]) {
set.add(v);
}
if (!firstSet[ch].has(EMPTY)) break;
}
// 2. 若First(BCD) 包含 ε,则Select(A)= First(BCD) 和 Follow(A) 的并集合去掉 ε。若不包含 Select(A) = First(BCD)
if (set.has(EMPTY)) {
selectSet[`${nts} -> ${grammarArr.join(" ")}`] = new Set(
[...set, ...followSet[nts]].filter((v) => v !== EMPTY)
);
} else {
selectSet[`${nts} -> ${grammarArr.join(" ")}`] = set;
}
}
}
};
LL1 集合
利用 Select 集合填上即可
const processLL1 = () => {
for (let nts of nonTermialSymbol) {
LL1Table[nts] = {};
}
Object.keys(selectSet).forEach((key) => {
const vt = key.split(" ->")[0];
const set = selectSet[key];
for (let v of set) {
if (typeof LL1Table[vt][v] !== "undefined") {
console.warn("---------------");
console.warn(
"请检查是否文法中含有二义性文法, 是否消除了左递归以及提取左公因子"
);
console.warn(`${key} 和 ${LL1Table[vt][v]}, 在 ${v} 存在 LL1 表冲突`);
console.warn("---------------");
return;
}
LL1Table[vt][v] = key;
}
});
};
结果
最后得到的结果如下
Terminal | First
S | { d ε b e a }
H | { e ε }
K | { d ε }
L | { e }
M | { d ε b }
Terminal | Follow
S | { $ o }
H | { $ o f }
K | { $ o e }
L | { d b e a o $ }
M | { $ o e }
grammar | Select
S -> M H | { d b e $ o }
S -> a | { a }
H -> L S o | { e }
H -> ε | { $ o f }
K -> d M L | { d }
K -> ε | { $ o e }
L -> e H f | { e }
M -> K | { d $ o e }
M -> b L M | { b }
LL1 | a | b | d | e | o | $ | f
S | S -> a | S -> M H | S -> M H | S -> M H | S -> M H | S -> M H |
H | | | | H -> L S o | H -> ε | H -> ε | H -> ε
K | | | K -> d M L | K -> ε | K -> ε | K -> ε |
L | | | | L -> e H f | | |
M | | M -> b L M | M -> K | M -> K | M -> K | M -> K |