用C++手把手实现LL(1)分析器:从文法解析到预测分析表生成(附完整代码)

张开发
2026/6/9 16:08:49 15 分钟阅读
用C++手把手实现LL(1)分析器:从文法解析到预测分析表生成(附完整代码)
从零构建LL(1)分析器C实战与编译原理精要在编译原理的殿堂里语法分析器扮演着翻译官的角色而LL(1)分析器则是其中最优雅的解决方案之一。本文将带你用C从零开始构建完整的LL(1)分析器不仅涵盖核心算法实现更会揭示工程实践中的关键技巧。无论你是正在学习编译原理的学生还是对语言处理感兴趣的技术爱好者这趟实现之旅都将让你获得远超理论知识的实战能力。1. LL(1)分析器核心架构设计LL(1)分析器的魅力在于其清晰的层次结构。我们需要构建的不仅仅是一堆代码而是一个完整的语法处理系统。整个架构可以分为三个关键层次文法处理层负责文法的读取、存储和预处理集合计算层构建FIRST集、FOLLOW集和SELECT集分析执行层实现预测分析表和字符串分析在C实现中我们采用面向对象的设计思想将每个非终结符抽象为数据结构struct Production { string head; // 产生式头部 vectorstring body; // 产生式体 setchar selectSet; // SELECT集合 }; class NonTerminal { public: char symbol; setchar firstSet; setchar followSet; vectorProduction productions; };这种设计不仅符合编译原理的数学本质也为后续的集合计算提供了便利。文法的存储采用链式结构可以高效处理文法规则的动态增删。2. 文法预处理消除左递归与左因子文法的规范化是LL(1)分析的前提条件。我们需要处理两种常见问题左递归和左因子。2.1 消除直接左递归直接左递归的消除算法可以形式化为以下步骤对于形如A → Aα|β的产生式引入新的非终结符A重写为A → βAA → αA|ε在C中的实现要点void eliminateLeftRecursion(NonTerminal nt) { vectorProduction newProductions; NonTerminal newNT; // 新建非终结符 for (auto prod : nt.productions) { if (prod.body[0] nt.symbol) { // 处理左递归产生式 Production newProd; newProd.head newNT.symbol; newProd.body.assign(prod.body.begin()1, prod.body.end()); newProd.body.push_back(newNT.symbol); newNT.productions.push_back(newProd); } else { // 处理非左递归产生式 prod.body.push_back(newNT.symbol); newProductions.push_back(prod); } } // 添加ε产生式 newNT.productions.push_back({newNT.symbol, {ε}, {}}); nt.productions newProductions; }2.2 提取左因子左因子提取的关键在于发现并分离公共前缀void factorize(NonTerminal nt) { mapstring, vectorProduction* prefixMap; // 按首符号分组 for (auto prod : nt.productions) { string firstSym prod.body.empty() ? ε : prod.body[0]; prefixMap[firstSym].push_back(prod); } // 处理需要提取左因子的情况 for (auto [prefix, prods] : prefixMap) { if (prods.size() 1 prefix ! ε) { NonTerminal newNT createNewNonTerminal(); Production factoredProd; factoredProd.head nt.symbol; factoredProd.body {prefix, newNT.symbol}; // 重写产生式 for (auto prod : prods) { Production newProd; newProd.head newNT.symbol; if (prod-body.size() 1) { newProd.body.assign(prod-body.begin()1, prod-body.end()); } else { newProd.body {ε}; } newNT.productions.push_back(newProd); } // 更新原始非终结符的产生式 nt.productions.erase( remove_if(nt.productions.begin(), nt.productions.end(), [](const Production p) { return find(prods.begin(), prods.end(), p) ! prods.end(); }), nt.productions.end()); nt.productions.push_back(factoredProd); } } }3. 关键集合计算FIRST、FOLLOW与SELECT集合计算是LL(1)分析器的数学核心正确的实现直接影响整个分析器的准确性。3.1 FIRST集计算算法FIRST集的递归计算需要特别注意ε处理void calculateFirstSets(Grammar grammar) { bool changed; do { changed false; for (auto nt : grammar.nonTerminals) { for (auto prod : nt.productions) { size_t beforeSize nt.firstSet.size(); bool canHaveEpsilon true; for (const auto symbol : prod.body) { if (isTerminal(symbol)) { nt.firstSet.insert(symbol[0]); canHaveEpsilon false; break; } else { auto nextNT grammar.getNT(symbol[0]); nt.firstSet.insert(nextNT.firstSet.begin(), nextNT.firstSet.end()); if (!nextNT.hasEpsilon()) { canHaveEpsilon false; break; } } } if (canHaveEpsilon) { nt.firstSet.insert(ε); } if (nt.firstSet.size() beforeSize) { changed true; } } } } while (changed); }3.2 FOLLOW集计算策略FOLLOW集的计算需要多次迭代直到收敛void calculateFollowSets(Grammar grammar) { grammar.startSymbol.followSet.insert($); // 结束符 bool changed; do { changed false; for (auto nt : grammar.nonTerminals) { for (auto prod : nt.productions) { for (size_t i 0; i prod.body.size(); i) { if (!isNonTerminal(prod.body[i])) continue; auto currentNT grammar.getNT(prod.body[i][0]); size_t beforeSize currentNT.followSet.size(); if (i 1 prod.body.size()) { string nextSym prod.body[i1]; if (isTerminal(nextSym)) { currentNT.followSet.insert(nextSym[0]); } else { auto nextNT grammar.getNT(nextSym[0]); currentNT.followSet.insert(nextNT.firstSet.begin(), nextNT.firstSet.end()); currentNT.followSet.erase(ε); if (nextNT.hasEpsilon()) { currentNT.followSet.insert(nt.followSet.begin(), nt.followSet.end()); } } } else { currentNT.followSet.insert(nt.followSet.begin(), nt.followSet.end()); } if (currentNT.followSet.size() beforeSize) { changed true; } } } } } while (changed); }3.3 SELECT集与LL(1)条件验证SELECT集是判断文法是否为LL(1)的关键void calculateSelectSets(Grammar grammar) { for (auto nt : grammar.nonTerminals) { for (auto prod : nt.productions) { bool canHaveEpsilon true; for (const auto symbol : prod.body) { if (isTerminal(symbol)) { prod.selectSet.insert(symbol[0]); canHaveEpsilon false; break; } else { auto nextNT grammar.getNT(symbol[0]); prod.selectSet.insert(nextNT.firstSet.begin(), nextNT.firstSet.end()); prod.selectSet.erase(ε); if (!nextNT.hasEpsilon()) { canHaveEpsilon false; break; } } } if (canHaveEpsilon) { prod.selectSet.insert(nt.followSet.begin(), nt.followSet.end()); } } } // 验证LL(1)条件 for (auto nt : grammar.nonTerminals) { for (size_t i 0; i nt.productions.size(); i) { for (size_t j i1; j nt.productions.size(); j) { setchar intersection; set_intersection( nt.productions[i].selectSet.begin(), nt.productions[i].selectSet.end(), nt.productions[j].selectSet.begin(), nt.productions[j].selectSet.end(), inserter(intersection, intersection.begin())); if (!intersection.empty()) { throw runtime_error(文法不是LL(1)文法); } } } } }4. 预测分析表构建与字符串分析预测分析表是LL(1)分析器的决策核心它将所有计算成果转化为具体的分析动作。4.1 预测分析表的数据结构我们采用二维映射结构存储预测分析表class PredictiveTable { private: mapchar, mapchar, Production* table; // [非终结符][终结符]-产生式 public: void build(const Grammar grammar) { for (const auto nt : grammar.nonTerminals) { for (const auto prod : nt.productions) { for (char terminal : prod.selectSet) { table[nt.symbol][terminal] prod; } } } } const Production* getProduction(char nonTerminal, char terminal) const { auto it1 table.find(nonTerminal); if (it1 table.end()) return nullptr; auto it2 it1-second.find(terminal); return it2 it1-second.end() ? nullptr : it2-second; } };4.2 分析过程的实现细节字符串分析是LL(1)分析器的最终阶段需要注意栈操作和错误处理bool analyzeString(const Grammar grammar, const PredictiveTable table, const string input) { stackchar parseStack; parseStack.push($); parseStack.push(grammar.startSymbol.symbol); size_t inputPos 0; char lookahead input.empty() ? $ : input[inputPos]; cout left setw(20) 分析栈 setw(20) 剩余输入 动作 endl; while (!parseStack.empty()) { // 输出当前状态 printStack(parseStack); cout setw(20) input.substr(inputPos) ; char top parseStack.top(); parseStack.pop(); if (top $) { if (lookahead $) { cout 接受 endl; return true; } cout 错误意外输入剩余 endl; return false; } if (isTerminal(string(1, top))) { if (top lookahead) { cout 匹配 top endl; inputPos; lookahead inputPos input.size() ? input[inputPos] : $; } else { cout 错误期望 top , 得到 lookahead endl; return false; } } else { // 非终结符 const Production* prod table.getProduction(top, lookahead); if (!prod) { cout 错误无 top - ... 的产生式对应输入 lookahead endl; return false; } cout top - ; for (const auto sym : prod-body) { cout sym ; } cout endl; if (prod-body[0] ! ε) { for (auto it prod-body.rbegin(); it ! prod-body.rend(); it) { parseStack.push((*it)[0]); } } } } return false; }5. 工程实践调试技巧与性能优化在实际开发中我们还需要考虑以下工程实践问题5.1 文法输入格式设计良好的输入格式可以大大提升用户体验# 注释以#开头 E - T E E - T E | ε T - F T T - * F T | ε F - ( E ) | id对应的解析代码Grammar parseGrammar(istream input) { Grammar grammar; string line; while (getline(input, line)) { // 跳过注释和空行 line line.substr(0, line.find(#)); if (line.empty()) continue; // 解析产生式 size_t arrowPos line.find(-); if (arrowPos string::npos) continue; char head line[0]; string bodyStr line.substr(arrowPos 2); // 处理多个选择 vectorstring alternatives; size_t pipePos; while ((pipePos bodyStr.find(|)) ! string::npos) { alternatives.push_back(trim(bodyStr.substr(0, pipePos))); bodyStr bodyStr.substr(pipePos 1); } alternatives.push_back(trim(bodyStr)); // 添加到文法 NonTerminal nt grammar.getOrCreateNT(head); for (const auto alt : alternatives) { Production prod; prod.head head; if (alt ε) { prod.body {}; } else { // 分割符号 stringstream ss(alt); string symbol; while (ss symbol) { prod.body.push_back(symbol); } } nt.productions.push_back(prod); } } return grammar; }5.2 可视化调试输出为关键步骤添加可视化输出有助于调试void printGrammar(const Grammar grammar) { cout 文法 \n; for (const auto nt : grammar.nonTerminals) { cout nt.symbol - ; for (size_t i 0; i nt.productions.size(); i) { if (i 0) cout | ; const auto prod nt.productions[i]; if (prod.body.empty()) { cout ε; } else { for (const auto sym : prod.body) { cout sym ; } } } cout endl; } } void printFirstSets(const Grammar grammar) { cout \n FIRST集 \n; for (const auto nt : grammar.nonTerminals) { cout FIRST( nt.symbol ) { ; for (char c : nt.firstSet) { cout c ; } cout }\n; } } void printPredictiveTable(const PredictiveTable table, const Grammar grammar) { cout \n 预测分析表 \n; // 收集所有终结符 setchar terminals; for (const auto nt : grammar.nonTerminals) { for (const auto prod : nt.productions) { for (char c : prod.selectSet) { terminals.insert(c); } } } terminals.insert($); // 打印表头 cout setw(10) NT\\T; for (char t : terminals) { cout setw(15) t; } cout endl; // 打印表格内容 for (const auto nt : grammar.nonTerminals) { cout setw(10) nt.symbol; for (char t : terminals) { const Production* prod table.getProduction(nt.symbol, t); if (prod) { string prodStr string(1, nt.symbol) - ; if (prod-body.empty()) { prodStr ε; } else { for (const auto sym : prod-body) { prodStr sym ; } } cout setw(15) prodStr; } else { cout setw(15) ; } } cout endl; } }5.3 性能优化技巧对于大型文法可以采用以下优化策略记忆化FIRST集计算缓存中间结果避免重复计算增量式FOLLOW集更新只重新计算受影响的部分符号表索引使用哈希表快速查找非终结符位集合表示对于固定符号集使用bitset提高集合操作效率// 使用bitset优化集合操作的示例 const int SYMBOL_COUNT 128; // ASCII范围 using SymbolSet bitsetSYMBOL_COUNT; class OptimizedNonTerminal { public: char symbol; SymbolSet firstSet; SymbolSet followSet; vectorProduction productions; bool hasEpsilon() const { return firstSet.test(ε); } void addToFirstSet(const SymbolSet other) { firstSet | other; } void addToFollowSet(const SymbolSet other) { followSet | other; } };6. 常见问题与解决方案在实际实现过程中开发者常会遇到以下典型问题6.1 左递归消除不彻底症状程序陷入无限循环或报错文法不是LL(1)解决方案确保处理了间接左递归检查非终结符的排序是否正确添加调试输出显示每一步的文法变化void eliminateIndirectLeftRecursion(Grammar grammar) { // 对非终结符进行排序 vectorchar ntOrder; for (const auto nt : grammar.nonTerminals) { ntOrder.push_back(nt.symbol); } for (size_t i 0; i ntOrder.size(); i) { char A ntOrder[i]; auto ntA grammar.getNT(A); for (size_t j 0; j i; j) { char B ntOrder[j]; vectorProduction newProductions; for (auto prod : ntA.productions) { if (!prod.body.empty() prod.body[0][0] B) { // 替换产生式 A → Bα auto ntB grammar.getNT(B); for (auto bProd : ntB.productions) { Production newProd; newProd.head A; if (bProd.body.empty()) { // B → ε newProd.body.assign(prod.body.begin()1, prod.body.end()); } else { newProd.body bProd.body; newProd.body.insert(newProd.body.end(), prod.body.begin()1, prod.body.end()); } newProductions.push_back(newProd); } } else { newProductions.push_back(prod); } } ntA.productions newProductions; } // 消除A的直接左递归 eliminateDirectLeftRecursion(ntA); } }6.2 FIRST集计算不收敛症状程序长时间运行或集合持续增长解决方案检查ε产生式的处理是否正确验证文法是否有循环依赖设置最大迭代次数防止无限循环void calculateFirstSetsWithLimit(Grammar grammar, int maxIterations 100) { int iteration 0; bool changed; do { changed false; iteration; if (iteration maxIterations) { throw runtime_error(FIRST集计算未收敛可能存在循环依赖); } for (auto nt : grammar.nonTerminals) { size_t beforeSize nt.firstSet.count(); for (auto prod : nt.productions) { bool allHaveEpsilon true; for (const auto symbol : prod.body) { if (isTerminal(symbol)) { nt.firstSet.set(symbol[0]); allHaveEpsilon false; break; } else { auto nextNT grammar.getNT(symbol[0]); nt.firstSet | nextNT.firstSet; if (!nextNT.hasEpsilon()) { allHaveEpsilon false; break; } } } if (allHaveEpsilon) { nt.firstSet.set(ε); } } if (nt.firstSet.count() beforeSize) { changed true; } } } while (changed); }6.3 预测分析表冲突症状同一单元格被多次赋值解决方案严格验证SELECT集不相交检查FIRST和FOLLOW集计算是否正确确保文法预处理彻底void verifyPredictiveTable(const PredictiveTable table, const Grammar grammar) { for (const auto nt : grammar.nonTerminals) { mapchar, const Production* terminalToProd; for (const auto prod : nt.productions) { for (char terminal : prod.getSelectSet()) { if (terminalToProd.count(terminal)) { cerr 冲突发现: nt.symbol 在输入 terminal 有多个产生式:\n; cerr 1. terminalToProd[terminal]-toString() \n; cerr 2. prod.toString() \n; throw runtime_error(预测分析表存在冲突); } terminalToProd[terminal] prod; } } } cout 预测分析表验证通过无冲突\n; }7. 扩展功能与进阶方向完成基础LL(1)分析器后可以考虑以下扩展方向7.1 错误恢复机制增强分析器的容错能力class ErrorRecovery { public: enum Strategy { PANIC_MODE, PHRASE_LEVEL, ERROR_PRODUCTION }; void handleError(ParsingContext context, Strategy strategy) { switch (strategy) { case PANIC_MODE: panicMode(context); break; case PHRASE_LEVEL: phraseLevel(context); break; case ERROR_PRODUCTION: errorProduction(context); break; } } private: void panicMode(ParsingContext ctx) { // 跳过输入直到同步符号 while (!ctx.isAtEnd() !isSyncSymbol(ctx.currentToken())) { ctx.advance(); } // 弹出栈直到恢复点 while (!ctx.stack.empty() !canContinueWith(ctx.stack.top(), ctx.currentToken())) { ctx.stack.pop(); } } bool isSyncSymbol(char token) { // 定义同步符号集 static const setchar syncSymbols {;, }, ), $}; return syncSymbols.count(token); } };7.2 语法树生成扩展分析器以构建抽象语法树(AST):class ASTNode { public: string type; string value; vectorunique_ptrASTNode children; void addChild(unique_ptrASTNode child) { children.push_back(move(child)); } }; class ASTBuilder { public: unique_ptrASTNode buildFromProduction(const Production prod) { auto node make_uniqueASTNode(); node-type prod.head; for (const auto symbol : prod.body) { auto child make_uniqueASTNode(); if (isTerminal(symbol)) { child-type TERMINAL; child-value symbol; } else { child-type symbol; } node-addChild(move(child)); } return node; } };7.3 性能分析与优化添加性能监控功能class Profiler { public: void startPhase(const string name) { phases[name] {chrono::high_resolution_clock::now(), {}}; } void endPhase(const string name) { auto phase phases[name]; phase.second chrono::high_resolution_clock::now() - phase.first; } void report() const { cout \n 性能分析 \n; for (const auto [name, data] : phases) { auto duration data.second; cout setw(20) name : duration.count() ns\n; } } private: mapstring, pairchrono::time_pointchrono::high_resolution_clock, chrono::nanoseconds phases; };实现LL(1)分析器的过程就像搭建一座精密的钟表每个齿轮都必须完美配合。从文法预处理到集合计算再到预测分析表的构建每一步都需要严谨的数学思维和细致的工程实现。当看到分析器成功识别出第一个语法正确的句子时那种成就感是对所有努力的最佳回报。

更多文章