LL(1) 语法分析器:从理论到 C++ 工程实践
在编译器的构建过程中,语法分析器扮演着至关重要的角色。LL(1) 作为最经典的自顶向下分析方法,以其清晰的逻辑结构和高效的线性时间复杂度,成为许多编译器前端的选择。本文将带你深入 LL(1) 分析器的实现细节,通过 500 行 C++ 代码的完整实现,揭示 First/Follow 集计算与预测分析表构建的核心算法。
1. LL(1) 分析器的核心架构
LL(1) 分析器的实现可分为三个关键模块:
- 文法预处理 :处理文法中的空产生式、左递归等问题
- 集合计算 :构建 First 集和 Follow 集
- 分析引擎 :基于预测分析表的语法分析
典型的类设计如下:
class Grammar {
set<string> nonTerminals; // 非终结符集合
set<string> terminals; // 终结符集合
string startSymbol; // 开始符号
map<string, vector<vector<string>>> productions; // 产生式规则
void eliminateLeftRecursion();
void leftFactor();
};
class LL1Analyzer {
map<string, set<string>> firstSets;
map<string, set<string>> followSets;
map<pair<string, string>, vector<string>> parsingTable;
void computeFirstSets();
void computeFollowSets();
void buildParsingTable();
public:
bool parse(const vector<string>& input);
};
2. First 集计算的两种实现范式
2.1 递归求解法
递归方法直接根据 First 集的定义实现:
set<string> LL1Analyzer::computeFirst(const string& symbol) {
if (isTerminal(symbol)) {
return {symbol}; // 终结符的 First 集是其本身
}
set<string> first;
for (auto& production : productions[symbol]) {
bool allNullable = true;
for (auto& s : production) {
auto sFirst = computeFirst(s);
first.insert(sFirst.begin(), sFirst.end());
if (sFirst.find("ε") == sFirst.end()) {
allNullable = false;
break;
}
}
if (allNullable) {
first.insert("ε");
}
}
return first;
}
2.2 迭代求解法
迭代法通过不断传播 First 元素直到收敛:
void LL1Analyzer::computeFirstSets() {
// 初始化
for (auto& nt : nonTerminals) {
firstSets[nt] = {};
}
bool changed;
do {
changed = false;
for (auto& [head, bodies] : productions) {
for (auto& body : bodies) {
size_t oldSize = firstSets[head].size();
bool nullable = true;
for (auto& symbol : body) {
if (isTerminal(symbol)) {
firstSets[head].insert(symbol);
nullable = false;
break;
}
firstSets[head].insert(
firstSets[symbol].begin(),
firstSets[symbol].end()
);
if (firstSets[symbol].find("ε") == firstSets[symbol].end()) {
nullable = false;
break;
}
}
if (nullable) {
firstSets[head].insert("ε");
}
if (firstSets[head].size() > oldSize) {
changed = true;
}
}
}
} while (changed);
}
两种方法的对比:
| 特性 | 递归法 | 迭代法 |
|---|---|---|
| 实现复杂度 | 简单直观 | 需要显式处理收敛条件 |
| 性能 | 可能重复计算 | 效率更高 |
| 适用场景 | 小型文法 | 大型复杂文法 |
| 空产生式处理 | 需要额外标记 | 自然融入迭代过程 |
3. Follow 集计算的关键技巧
Follow 集计算需要特别注意文法开始符号和产生式右部末尾的情况:
void LL1Analyzer::computeFollowSets() {
// 初始化
for (auto& nt : nonTerminals) {
followSets[nt] = {};
}
followSets[startSymbol].insert("$"); // 结束符号
bool changed;
do {
changed = false;
for (auto& [head, bodies] : productions) {
for (auto& body : bodies) {
for (size_t i = 0; i < body.size(); ++i) {
if (!isNonTerminal(body[i])) continue;
string A = body[i];
size_t oldSize = followSets[A].size();
// 情况1:A后面跟着符号B
if (i + 1 < body.size()) {
string B = body[i+1];
auto firstB = firstSets[B];
followSets[A].insert(firstB.begin(), firstB.end());
followSets[A].erase("ε");
// 如果B可推出空串,需要加上Follow(head)
if (firstB.find("ε") != firstB.end()) {
followSets[A].insert(
followSets[head].begin(),
followSets[head].end()
);
}
}
// 情况2:A是产生式最后一个符号
else {
followSets[A].insert(
followSets[head].begin(),
followSets[head].end()
);
}
if (followSets[A].size() > oldSize) {
changed = true;
}
}
}
}
} while (changed);
}
注意:Follow 集计算必须等待 First 集完全计算完成后才能开始,且需要多次迭代直到不再变化。
4. 预测分析表的构建策略
预测分析表是 LL(1) 分析器的核心数据结构,我们使用
unordered_map
实现高效的二维查找:
void LL1Analyzer::buildParsingTable() {
for (auto& [head, bodies] : productions) {
for (auto& body : bodies) {
auto firstAlpha = computeSequenceFirst(body);
// 规则1:对于First(α)中的每个终结符a,加入M[A,a]
for (auto& a : firstAlpha) {
if (a != "ε") {
parsingTable[{head, a}] = body;
}
}
// 规则2:如果ε在First(α)中,对Follow(A)的每个b加入M[A,b]
if (firstAlpha.find("ε") != firstAlpha.end()) {
for (auto& b : followSets[head]) {
parsingTable[{head, b}] = body;
}
}
}
}
// 处理错误条目(可选)
for (auto& nt : nonTerminals) {
for (auto& t : terminals) {
if (parsingTable.find({nt, t}) == parsingTable.end()) {
parsingTable[{nt, t}] = {"error"}; // 同步记号
}
}
}
}
预测分析表的可视化表示(以简单算术表达式文法为例):
| 非终结符 | id | + | * | ( | ) | $ |
|---|---|---|---|---|---|---|
| E | E→TE' | E→TE' | ||||
| E' | E'→+TE' | E'→ε | E'→ε | |||
| T | T→FT' | T→FT' | ||||
| T' | T'→ε | T'→*FT' | T'→ε | T'→ε | ||
| F | F→id | F→(E) |
5. 语法分析引擎的实现
分析引擎使用栈结构管理推导过程:
bool LL1Analyzer::parse(const vector<string>& input) {
stack<string> stk;
stk.push("$");
stk.push(startSymbol);
size_t inputPtr = 0;
string a = input[inputPtr];
while (!stk.empty()) {
string X = stk.top();
stk.pop();
if (isTerminal(X)) {
if (X == a) {
inputPtr++;
a = inputPtr < input.size() ? input[inputPtr] : "$";
} else {
return false; // 不匹配
}
}
else if (X == "$") {
if (X == a) {
return true; // 成功
} else {
return false;
}
}
else {
auto it = parsingTable.find({X, a});
if (it == parsingTable.end() || it->second[0] == "error") {
return false; // 分析表无条目
}
// 逆序压栈
const auto& production = it->second;
if (!(production.size() == 1 && production[0] == "ε")) {
for (auto it = production.rbegin(); it != production.rend(); ++it) {
stk.push(*it);
}
}
}
}
return false;
}
分析过程的追踪输出示例:
步骤 栈 输入 动作
1 $E id+id*id$ E → TE'
2 $E'T id+id*id$ T → FT'
3 $E'T'F id+id*id$ F → id
4 $E'T'id id+id*id$ 匹配id
5 $E'T' +id*id$ T' → ε
6 $E' +id*id$ E' → +TE'
7 $E'T+ +id*id$ 匹配+
8 $E'T id*id$ T → FT'
...
6. 工程实践中的优化技巧
6.1 避免递归死循环
在计算 First/Follow 集时,需要特别处理左递归文法:
void detectLeftRecursion() {
for (auto& [head, bodies] : productions) {
for (auto& body : bodies) {
if (!body.empty() && body[0] == head) {
throw runtime_error("直接左递归: " + head);
}
}
}
}
6.2 内存优化策略
对于大型文法,可以采用以下优化:
// 使用位集表示First/Follow集
class FastSet {
bitset<256> bits; // 假设终结符不超过256个
public:
void add(char c) { bits.set(c); }
bool contains(char c) const { return bits.test(c); }
// ... 其他集合操作
};
// 生产式编号替代完整产生式
map<pair<int, int>, int> compactParsingTable;
6.3 错误恢复机制
增强分析器的容错能力:
bool parseWithRecovery(const vector<string>& input) {
// ...
while (!stk.empty()) {
// ... 正常分析逻辑
if (分析表无条目) {
// 恐慌模式恢复
skipUntil(inputPtr, followSets[X]);
stk.pop(); // 弹出X
continue;
}
}
// ...
}
7. 完整项目结构与测试案例
项目目录结构建议:
LL1-Parser/
├── include/
│ ├── grammar.h # 文法定义与处理
│ ├── ll1_analyzer.h # 核心分析器类
│ └── utils.h # 工具函数
├── src/
│ ├── main.cpp # 测试程序
│ └── ll1_analyzer.cpp
├── test/
│ ├── arithmetic.txt # 算术表达式文法
│ └── test_cases/ # 测试用例
└── CMakeLists.txt
测试文法示例(arithmetic.txt):
E -> T E'
E' -> + T E' | ε
T -> F T'
T' -> * F T' | ε
F -> ( E ) | id
编译与运行:
mkdir build && cd build
cmake .. && make
./ll1_parser ../test/arithmetic.txt "id+id*id"
8. 扩展与进阶方向
- 语法树生成 :在分析过程中构建具体/抽象语法树
- 错误诊断 :增强错误定位与提示能力
- 自动化测试 :构建文法测试框架
- 性能分析 :对大型文法进行性能剖析
// 语法树节点示例
class ASTNode {
string type;
string value;
vector<ASTNode> children;
void print(int indent = 0) const {
cout << string(indent, ' ') << type;
if (!value.empty()) cout << ": " << value;
cout << endl;
for (auto& child : children) {
child.print(indent + 2);
}
}
};
实现完整的 LL(1) 分析器不仅需要理解形式语言理论,还需要考虑工程实践中的各种边界条件和性能因素。本文展示的代码框架已在 GitHub 开源项目中经过验证,能够正确处理教材中的典型文法案例。
&spm=1001.2101.3001.5002&articleId=89651228&d=1&t=3&u=f66e013b3b6041c5aa8078699267e125)
4024

被折叠的 条评论
为什么被折叠?



