告别枯燥理论:用C++ 11手把手实现一个LL(1)预测分析器(附完整源码)
从零构建LL(1)预测分析器:C++11实战指南
当你第一次翻开编译原理教材,看到那些晦涩的FIRST集、FOLLOW集和预测分析表时,是否感到一头雾水?别担心,这正是大多数初学者都会经历的阶段。本文将带你用C++11从零开始实现一个完整的LL(1)预测分析器,通过动手实践来真正理解这些抽象概念。我们不仅会实现核心算法,还会分享开发过程中的实用技巧和调试经验。
1. 环境准备与项目配置
1.1 开发工具选择
推荐使用CLion作为IDE,它提供了优秀的C++支持和对现代CMake项目的完美集成。对于编译器,建议选择MinGW-w64中的GCC 8.3.0或更高版本:
# 检查GCC版本
g++ --version
# 若未安装,可通过MinGW-w64安装
pacman -S mingw-w64-x86_64-gcc
1.2 CMake项目配置
创建一个基本的CMake项目结构:
LL1-Parser/
├── CMakeLists.txt
├── include/
│ ├── parser.h
│ └── rule_loader.h
├── src/
│ ├── main.cpp
│ ├── parser.cpp
│ └── rule_loader.cpp
└── test/
├── rules.txt
└── test_input.txt
对应的CMakeLists.txt基础配置:
cmake_minimum_required(VERSION 3.15)
project(LL1_Parser)
set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(LL1_Parser
src/main.cpp
src/parser.cpp
src/rule_loader.cpp
)
target_include_directories(LL1_Parser PRIVATE include)
2. 核心数据结构设计
2.1 文法规则表示
我们使用结构体来表示文法规则和相关集合:
// 文法规则类型:左部非终结符 → 右部产生式
using ProductionRule = std::pair<char, std::string>;
struct Grammar {
std::unordered_set<char> non_terminals; // 非终结符集合
std::unordered_set<char> terminals; // 终结符集合
std::vector<ProductionRule> rules; // 产生式规则
char epsilon = '$'; // 空串表示符号
char start_symbol; // 开始符号
};
2.2 符号集合表示
为每个符号设计集合存储结构:
struct SymbolSets {
std::unordered_set<char> first_set;
std::unordered_set<char> follow_set;
bool first_has_epsilon = false;
};
// 全局符号集合映射
using SymbolSetMap = std::unordered_map<char, SymbolSets>;
2.3 预测分析表实现
采用压缩存储的哈希表实现预测分析表:
class PredictionTable {
public:
void add_entry(char non_terminal, char terminal, int rule_idx);
int get_rule(char non_terminal, char terminal) const;
private:
// 将两个char压缩为uint16_t作为键
static uint16_t make_key(char first, char second) {
return (static_cast<uint16_t>(first) << 8) | second;
}
std::unordered_map<uint16_t, int> table_;
};
3. 关键算法实现
3.1 FIRST集计算
实现递归计算FIRST集的算法:
void calculate_first_sets(const Grammar& grammar, SymbolSetMap& symbol_sets) {
// 初始化终结符的FIRST集
for (char terminal : grammar.terminals) {
symbol_sets[terminal].first_set.insert(terminal);
}
// 非终结符FIRST集计算
bool changed;
do {
changed = false;
for (const auto& rule : grammar.rules) {
char left = rule.first;
const std::string& right = rule.second;
SymbolSets& left_sets = symbol_sets[left];
size_t before_size = left_sets.first_set.size();
// 处理产生式右部
bool all_has_epsilon = true;
for (char symbol : right) {
const auto& right_sets = symbol_sets[symbol];
// 添加FIRST(symbol)-{ε}
for (char first : right_sets.first_set) {
if (first != grammar.epsilon) {
left_sets.first_set.insert(first);
}
}
// 如果当前符号不包含ε,停止处理
if (!right_sets.first_has_epsilon) {
all_has_epsilon = false;
break;
}
}
// 如果所有符号都包含ε,则添加ε
if (all_has_epsilon) {
left_sets.first_has_epsilon = true;
left_sets.first_set.insert(grammar.epsilon);
}
// 检查是否发生变化
if (left_sets.first_set.size() != before_size) {
changed = true;
}
}
} while (changed);
}
3.2 FOLLOW集计算
实现迭代计算FOLLOW集的算法:
void calculate_follow_sets(const Grammar& grammar, SymbolSetMap& symbol_sets) {
// 初始化开始符号的FOLLOW集
symbol_sets[grammar.start_symbol].follow_set.insert('#');
bool changed;
do {
changed = false;
for (const auto& rule : grammar.rules) {
char left = rule.first;
const std::string& right = rule.second;
for (size_t i = 0; i < right.size(); ++i) {
char current = right[i];
if (grammar.terminals.count(current)) continue;
SymbolSets& current_sets = symbol_sets[current];
size_t before_size = current_sets.follow_set.size();
// 处理当前符号后面的部分
bool all_has_epsilon = true;
for (size_t j = i + 1; j < right.size(); ++j) {
char next = right[j];
const auto& next_sets = symbol_sets[next];
// 添加FIRST(next)-{ε}
for (char first : next_sets.first_set) {
if (first != grammar.epsilon) {
current_sets.follow_set.insert(first);
}
}
// 如果next不包含ε,停止处理
if (!next_sets.first_has_epsilon) {
all_has_epsilon = false;
break;
}
}
// 如果后面所有符号都包含ε,或者当前是最后一个符号
if (all_has_epsilon || i == right.size() - 1) {
// 添加左部的FOLLOW集
for (char follow : symbol_sets[left].follow_set) {
current_sets.follow_set.insert(follow);
}
}
// 检查是否发生变化
if (current_sets.follow_set.size() != before_size) {
changed = true;
}
}
}
} while (changed);
}
3.3 预测分析表构建
基于FIRST和FOLLOW集构建预测分析表:
void build_prediction_table(const Grammar& grammar,
const SymbolSetMap& symbol_sets,
PredictionTable& table) {
for (size_t i = 0; i < grammar.rules.size(); ++i) {
const auto& rule = grammar.rules[i];
char left = rule.first;
const std::string& right = rule.second;
// 计算产生式的FIRST集
std::unordered_set<char> first_alpha;
bool has_epsilon = true;
for (char symbol : right) {
const auto& sets = symbol_sets.at(symbol);
for (char first : sets.first_set) {
if (first != grammar.epsilon) {
first_alpha.insert(first);
}
}
if (!sets.first_has_epsilon) {
has_epsilon = false;
break;
}
}
if (has_epsilon) {
first_alpha.insert(grammar.epsilon);
}
// 添加表项
for (char terminal : first_alpha) {
if (terminal != grammar.epsilon) {
table.add_entry(left, terminal, i);
}
}
// 处理ε产生式
if (has_epsilon) {
for (char follow : symbol_sets.at(left).follow_set) {
table.add_entry(left, follow, i);
}
}
}
}
4. 总控程序实现
4.1 分析器核心逻辑
实现预测分析的总控程序:
class LL1Parser {
public:
LL1Parser(const Grammar& grammar, const PredictionTable& table)
: grammar_(grammar), table_(table) {}
bool parse(const std::string& input) {
std::stack<char> symbol_stack;
symbol_stack.push('#');
symbol_stack.push(grammar_.start_symbol);
size_t input_pos = 0;
while (!symbol_stack.empty()) {
char top = symbol_stack.top();
char current_input = input_pos < input.size() ? input[input_pos] : '#';
// 栈顶是终结符
if (grammar_.terminals.count(top) || top == '#') {
if (top == current_input) {
symbol_stack.pop();
input_pos++;
} else {
std::cerr << "Error: Mismatched terminal '" << top
<< "' with input '" << current_input << "'\n";
return false;
}
}
// 栈顶是非终结符
else {
int rule_idx = table_.get_rule(top, current_input);
if (rule_idx == -1) {
std::cerr << "Error: No rule for '" << top
<< "' with input '" << current_input << "'\n";
return false;
}
symbol_stack.pop();
const std::string& right = grammar_.rules[rule_idx].second;
// 逆向压栈(最左推导)
for (auto it = right.rbegin(); it != right.rend(); ++it) {
if (*it != grammar_.epsilon) {
symbol_stack.push(*it);
}
}
}
}
return true;
}
private:
const Grammar& grammar_;
const PredictionTable& table_;
};
4.2 文法文件解析
实现从规则文件加载文法的功能:
Grammar load_grammar_from_file(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("Cannot open grammar file");
}
Grammar grammar;
// 读取非终结符和终结符
std::string line;
std::getline(file, line);
for (char c : line) {
if (!isspace(c)) grammar.non_terminals.insert(c);
}
std::getline(file, line);
for (char c : line) {
if (!isspace(c)) grammar.terminals.insert(c);
}
// 读取产生式规则
while (std::getline(file, line)) {
if (line.empty()) continue;
std::istringstream iss(line);
char left;
std::string right;
if (iss >> left >> right) {
grammar.rules.emplace_back(left, right);
}
}
// 设置开始符号(假设第一个非终结符)
grammar.start_symbol = *grammar.non_terminals.begin();
return grammar;
}
5. 测试与调试技巧
5.1 测试用例设计
针对简单算术表达式文法设计测试用例:
# rules.txt 文件内容
EATBF +*()i
E TA
A +TA
A $
T FB
B *FB
B $
F (E)
F i
测试输入示例:
# 正确输入
i+i*i
(i+i)*i
i*(i+i)
# 错误输入
i+*i # 连续运算符
i+) # 不匹配括号
i+k # 非法符号
5.2 调试技巧
- 可视化输出集合 :在计算FIRST和FOLLOW集时,添加调试输出:
void print_sets(const SymbolSetMap& sets) {
for (const auto& [symbol, data] : sets) {
std::cout << symbol << ":\n";
std::cout << " FIRST: { ";
for (char c : data.first_set) std::cout << c << " ";
std::cout << "}";
if (data.first_has_epsilon) std::cout << " (has ε)";
std::cout << "\n";
std::cout << " FOLLOW: { ";
for (char c : data.follow_set) std::cout << c << " ";
std::cout << "}\n\n";
}
}
- 预测分析表验证 :检查表项是否冲突(LL(1)文法要求每个表项最多一个产生式)
void validate_table(const PredictionTable& table, const Grammar& grammar) {
for (char non_terminal : grammar.non_terminals) {
for (char terminal : grammar.terminals) {
if (terminal == grammar.epsilon) continue;
int rule1 = table.get_rule(non_terminal, terminal);
int rule2 = table.get_rule(non_terminal, terminal);
if (rule1 != rule2) {
std::cerr << "Conflict in table [" << non_terminal
<< "][" << terminal << "]\n";
}
}
}
}
- 逐步跟踪分析过程 :在总控程序中添加日志输出
// 在parse方法中添加调试输出
std::cout << "Stack: ";
for (std::stack<char> dump = symbol_stack; !dump.empty(); dump.pop()) {
std::cout << dump.top() << " ";
}
std::cout << "\nInput: " << input.substr(input_pos) << "\n\n";
6. 性能优化与扩展
6.1 性能优化技巧
- 集合运算优化 :使用位图表示符号集合,提高集合运算效率
class SymbolSet {
public:
void add(char c) { bitset_.set(char_to_index(c)); }
bool contains(char c) const { return bitset_.test(char_to_index(c)); }
private:
static size_t char_to_index(char c) {
return static_cast<size_t>(static_cast<unsigned char>(c));
}
std::bitset<256> bitset_;
};
- 预测分析表缓存 :将预测分析表序列化到文件,避免重复计算
6.2 功能扩展方向
- 错误恢复机制 :实现更友好的语法错误提示和恢复
enum class ErrorRecovery {
PANIC_MODE, // 跳过输入直到同步符号
PHRASE_LEVEL, // 局部修复
PRODUCT_LEVEL // 尝试替代产生式
};
class EnhancedParser : public LL1Parser {
public:
using LL1Parser::LL1Parser;
bool parse_with_recovery(const std::string& input,
ErrorRecovery strategy) {
// 实现错误恢复逻辑
}
};
- 语法树生成 :扩展分析器以构建具体语法树
struct SyntaxTreeNode {
std::string symbol;
std::vector<SyntaxTreeNode> children;
};
class TreeBuildingParser : public LL1Parser {
public:
SyntaxTreeNode get_syntax_tree() const { return syntax_tree_; }
private:
SyntaxTreeNode syntax_tree_;
};
- 支持更复杂文法 :扩展为LL(k)或处理左递归文法
7. 常见问题与解决方案
7.1 FIRST/FOLLOW集计算不收敛
问题现象 :计算过程陷入无限循环,集合不断增长。
解决方案 :
- 检查文法是否存在左递归
- 验证ε产生式处理是否正确
- 添加最大迭代次数限制
void calculate_first_sets(const Grammar& grammar, SymbolSetMap& symbol_sets) {
constexpr int MAX_ITERATIONS = 100;
int iterations = 0;
do {
// ...原有逻辑...
if (++iterations > MAX_ITERATIONS) {
throw std::runtime_error("FIRST set calculation not converging");
}
} while (changed);
}
7.2 预测分析表冲突
问题现象 :同一表项对应多个产生式。
解决方案 :
- 验证文法是否为LL(1)文法
- 检查FIRST和FOLLOW集计算是否正确
- 考虑文法变换消除冲突
7.3 内存消耗过大
问题现象 :处理大型文法时内存占用高。
优化方案 :
- 使用更紧凑的数据结构
- 采用惰性计算方法
- 对不常用数据序列化到磁盘
class DiskBackedSymbolSet {
public:
void add(char c) {
memory_cache_.insert(c);
if (memory_cache_.size() > CACHE_SIZE) {
flush_to_disk();
}
}
private:
static constexpr size_t CACHE_SIZE = 1000;
std::unordered_set<char> memory_cache_;
std::string disk_cache_file_;
void flush_to_disk() {
// 实现磁盘存储逻辑
}
};
8. 现代C++特性应用
8.1 使用智能指针管理资源
class GrammarManager {
public:
std::shared_ptr<Grammar> load_grammar(const std::string& filename) {
auto grammar = std::make_shared<Grammar>();
// 加载逻辑...
return grammar;
}
private:
std::vector<std::shared_ptr<Grammar>> loaded_grammars_;
};
8.2 利用移动语义提高性能
class PredictionTable {
public:
// 使用移动构造
PredictionTable(PredictionTable&& other) noexcept
: table_(std::move(other.table_)) {}
// 使用移动赋值
PredictionTable& operator=(PredictionTable&& other) noexcept {
table_ = std::move(other.table_);
return *this;
}
};
8.3 使用Lambda简化集合运算
void process_symbol_set(const SymbolSet& set,
std::function<void(char)> action) {
for (char c : set) {
action(c);
}
}
// 使用示例
process_symbol_set(first_set, [&](char c) {
if (c != epsilon) {
table.add_entry(non_terminal, c, rule_idx);
}
});
9. 工程实践建议
9.1 单元测试策略
为每个核心组件编写单元测试:
TEST_CASE("FIRST set calculation") {
Grammar grammar = {
{'E', 'T'}, {'E', 'F'}, // 测试文法
{'T', 'a'}, {'F', 'b'}
};
SymbolSetMap sets;
calculate_first_sets(grammar, sets);
REQUIRE(sets['E'].first_set == std::unordered_set<char>{'a', 'b'});
REQUIRE(sets['T'].first_set == std::unordered_set<char>{'a'});
}
9.2 性能基准测试
使用Google Benchmark进行性能分析:
static void BM_FirstSetCalculation(benchmark::State& state) {
Grammar grammar = create_large_grammar();
SymbolSetMap sets;
for (auto _ : state) {
calculate_first_sets(grammar, sets);
}
}
BENCHMARK(BM_FirstSetCalculation);
9.3 持续集成配置
示例GitHub Actions配置:
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install dependencies
run: sudo apt-get install g++ cmake
- name: Build
run: |
mkdir build
cd build
cmake ..
make
- name: Test
run: cd build && ctest --output-on-failure
10. 从理论到实践的思考
实现编译器的前端组件是理解编程语言本质的绝佳途径。在完成这个LL(1)预测分析器的过程中,有几个关键点值得注意:
-
算法选择的重要性 :FIRST和FOLLOW集的迭代计算方法虽然简单,但对于大型文法可能效率不高。在实际工业级编译器中,往往会采用更高效的图算法或矩阵运算。
-
错误处理的用户体验 :学术实现通常只关注正确性,而实际工程中需要精心设计错误恢复机制和友好的错误信息。
-
测试覆盖的全面性 :文法规则的微小变化可能导致分析器行为完全不同,需要建立完善的测试套件。
-
性能与可维护性的平衡 :初期实现可以使用简单直观的数据结构,随着项目复杂度的增加,再逐步引入更高效的实现。
-
现代语言特性的合理运用 :C++11及后续标准提供的特性可以大幅提升代码质量和开发效率,但要注意不要过度设计。
更多推荐
所有评论(0)