语法分析器源码包(Code::Blocks工程+文档+测试用例))
本文还有配套的精品资源点击获取简介山东科技大学编译原理课程实验三的LL(1)语法分析器完整实现基于C开发直接适配Code::Blocks IDE。包含可一键编译运行的.cbpp工程文件、主程序main.cpp、项目依赖与布局配置以及预置的Debug和bin目录结构。配套Word文档详细说明实验要求、给定文法含E/T/F/G/M等非终结符、FIRST集与FOLLOW集推导过程、预测分析表构造步骤和错误处理逻辑。提供测试数据.txt内含iii、(ii)/i、i(ii)等典型输入串覆盖合法推导、嵌套括号、运算符优先级及非法字符等边界场景支持分析过程跟踪与报错提示。代码已消除左递归、提取左公因子严格满足LL(1)文法判定条件能正确执行栈驱动的匹配、推导模拟、符号回退及语法错误定位。1. 项目概述这不是一个“交作业式”的LL(1)实现而是一套可调试、可追踪、可教学的语法分析沙盒如果你正在山东科技大学或任何一所开设编译原理课程的高校修这门课刚拿到实验三的要求——“实现一个LL(1)语法分析器”大概率会经历三个阶段第一阶段是翻教材、查定义对着“FIRST集”“FOLLOW集”“预测分析表”这些词发懵第二阶段是照着伪代码硬写结果输入一个ii*i就栈溢出或者遇到(直接崩溃第三阶段也是最关键的阶段——你终于跑通了但完全不知道中间哪一步推导错了、为什么报错位置总比实际错的地方晚一个字符、为什么i*(ii)能过而i*(ii却卡死在$上……这时候你真正需要的不是一份能“交差”的代码而是一个透明的、可单步观察的、每一步都有依据可查的语法分析过程沙盒。这个资源包就是为解决第三阶段的困惑而生的。它不是一个黑盒程序而是一整套“可解剖”的LL(1)实践系统。核心关键词——LL(1)分析器、编译原理实验、语法分析实现、Code::Blocks工程——每一个都指向它的设计意图它必须能被学生在本地IDE里打开、打断点、看变量值、改文法、重算FIRST/FOLLOW、重新生成分析表并立刻看到效果变化。它不追求工业级的健壮性比如支持Unicode标识符或宏展开但极度强调教学级的可验证性与可追溯性。比如文档里写的FOLLOW(E) { , ), $ }你在调试时就能在followSet[E]这个vector里亲眼看到这三个元素比如预测分析表中M[T, i]对应的是T → F T这条产生式你就能在predictTable[T][i]这个map里查到它的索引号再顺着索引找到productions[2]的具体内容。这种“所见即所得”的映射关系是绝大多数开源LL(1)示例缺失的关键一环。它用C而非Python或Java不是为了炫技而是因为C的指针、vector下标、map键值对在调试器里呈现得最直观它绑定Code::Blocks是因为它默认使用MinGW没有复杂的运行时依赖双击.cbp文件就能进调试界面连环境变量都不用配。我当年带实验课时反复强调语法分析不是背公式而是理解“栈顶符号”和“当前输入符号”之间那张表如何指挥整个推导流程。这个包就是把这张表从纸面搬进了你的IDE调试窗口。2. 整体设计思路与方案选型解析为什么是“栈驱动查表状态机”而不是递归下降2.1 核心架构选择显式栈 vs 隐式调用栈LL(1)分析器有两种主流实现路径递归下降Recursive Descent和表驱动Table-Driven。本项目坚定选择了后者原因非常务实教学可视化。递归下降将文法结构直接映射为函数调用链逻辑清晰但调试时你看到的是一层层嵌套的函数帧parseE()调parseT()再调parseF()而FIRST/FOLLOW集、预测分析表这些核心概念在函数体内是隐式的、分散的。学生很难把if (lookahead i) parseF(); else if (lookahead () parseF();这段代码和文档里那个3×5的二维预测分析表联系起来。而表驱动方式强制将所有决策逻辑收束到一张mapchar, mapchar, int predictTable里。主循环只有四行核心逻辑while (!stack.empty() !input.empty()) { char top stack.back(); stack.pop_back(); char lookahead input[0]; if (isTerminal(top)) { if (top lookahead) input.erase(0, 1); // 匹配成功 else error(期待 string(1, top) , 得到 string(1, lookahead)); } else { int prodIdx predictTable[top][lookahead]; // 查表关键一步 if (prodIdx -1) error(无匹配产生式输入符号: string(1, lookahead)); applyProduction(prodIdx); // 将产生式右部逆序压栈 } }这段代码就是LL(1)算法的灵魂。predictTable[top][lookahead]这个操作完美对应了教材里“M[A, a] α”的定义。学生在调试时只要把鼠标悬停在prodIdx变量上就能立刻看到查表的结果再点开productions[prodIdx]就能看到完整的产生式字符串。这种“决策点高度集中、行为高度可观察”的设计是教学场景下的最优解。2.2 文法预处理消除左递归与提取左公因子的不可跳过性给定文法我们以实验文档中的典型文法为例E → E T | T T → T * F | F F → ( E ) | i G → E | ε // 假设存在空产生式 M → i M | i // 假设存在左递归直接拿这个文法去构造FIRST/FOLLOW集会得到灾难性结果。E → E T是典型的直接左递归会导致预测分析表中M[E, ]和M[E, i]出现冲突都可能填入E → E T或E → T。M → i M | i是间接左递归的变体同样破坏LL(1)条件。而T → T * F | F和E → E T | T这类结构如果不处理FOLLOW(T)会包含和*导致M[T, ]和M[T, *]冲突。因此预处理不是可选项而是LL(1)实现的前置生死线。本项目采用标准算法-消除直接左递归对形如A → Aα | β的规则替换为A → βA,A → αA | ε。-提取左公因子对形如A → αβ1 | αβ2的规则替换为A → αA,A → β1 | β2。以E → E T | T为例消除后变为E → T E E → T E | ε这个变换不是数学游戏。它直接决定了后续FOLLOW集的计算结果。FOLLOW(E’) 必须包含 FOLLOW(E) 和)因为E’出现在E的右部且E后面可能跟)而FOLLOW(E)又由文法中E出现的位置决定如F → ( E )则)∈ FOLLOW(E)。文档中详尽的手工推导过程其价值就在于让学生亲手走一遍这个链条文法变换 → FIRST集变化 → FOLLOW集变化 → 预测分析表冲突是否消失。我见过太多学生跳过这一步直接抄表结果在ii*i上永远卡在E的ε产生式选择上——因为他们没算出$和)都在FOLLOW(E’)里所以M[E’, $]和M[E’, )]都该填ε而不是只填一个。2.3 数据结构选型为什么用mapchar, mapchar, int而不是二维数组初学者常问预测分析表明明是个矩阵为什么不用int table[128][128]答案是稀疏性与可维护性。我们的终结符集合i,,*,(,),$和非终结符集合E,E,T,T,F,G,M都很小但ASCII码范围是0-127。用二维数组99%的空间都是浪费的而且table[E][]这种写法在C里是未定义行为E是69是43数组下标越界。mapchar, mapchar, int提供了安全的键值查找predictTable[E][]会自动返回0如果未设置或你存入的产生式索引。更重要的是它天然支持“查无此条目”的语义——当predictTable[top][lookahead]返回0而0又不是有效产生式索引我们约定-1为空这就构成了一个清晰的错误信号“查表失败”。如果用数组你需要额外维护一个valid[128][128]布尔矩阵代码陡增且易错。此外map的键是char意味着你可以直接用文法符号本身作为索引代码可读性极高predictTable[T][i]比table[2][3]假设T是第2个非终结符i是第3个终结符直观一万倍。这正是教学代码该有的样子让数据结构本身成为概念的注释。3. 核心细节解析与实操要点从FIRST/FOLLOW推导到预测分析表生成的完整闭环3.1 FIRST集推导不只是“向下找”更要理解“何时停止”FIRST(X)的定义是从X出发能推导出的所有以终结符开头的串的首符号集合若X能推导出ε则ε也属于FIRST(X)。推导过程看似简单但学生最容易犯两个致命错误错误一对ε的传递性理解不足。例如有规则A → B C,B → ε,C → d。学生常以为FIRST(A) FIRST(B) {ε}忽略了B能推出ε所以A的推导可以继续到C。正确过程是FIRST(A) (FIRST(B) \ {ε}) ∪ (如果ε∈FIRST(B)则FIRST(C)) ∅ ∪ FIRST(C) {d}。本项目代码中computeFirst()函数用了一个changed标志位循环迭代直到所有FIRST集稳定。它先初始化所有终结符的FIRST集为其自身所有非终结符的FIRST集为空然后反复扫描所有产生式对A → X1 X2 ... Xn将FIRST(X1)加入FIRST(A)如果ε∈FIRST(X1)再加入FIRST(X2)依此类推。这个迭代过程就是对ε传递性的程序化模拟。错误二忽略“终结符自身就是其FIRST集”。FIRST(i)就是{i}不是空集也不是{ε}。这是一个基础但关键的锚点。代码中firstSet[i] {i}是硬编码的起点所有非终结符的FIRST集都由此生长。文档里手推的FIRST集每一行都应该能对应到代码中某次insert()调用。提示在Code::Blocks中调试computeFirst()时可以在循环内加一个断点观察firstSet[E]是如何从空集一步步被i、(填充进去的。这是理解整个推导链条最直观的方式。3.2 FOLLOW集推导理解“谁在后面”比“怎么算”更重要FOLLOW(A)的定义是在某个句型中紧跟在A后面的终结符的集合。它的计算规则比FIRST复杂核心在于三条1. 对于起始符号S$∈ FOLLOW(S)。2. 若有产生式B → α A β则 FIRST(β) \ {ε} ⊆ FOLLOW(A)。3. 若有产生式B → α A或B → α A β且 ε ∈ FIRST(β)则 FOLLOW(B) ⊆ FOLLOW(A)。第二条规则是学生最易混淆的。B → α A β中的β是A右边的所有符号不是单个符号。例如E → T E这里α是TA是Eβ是空因为E’后面没东西了所以规则3触发FOLLOW(E) ⊆ FOLLOW(E’)。而E → T Eα是 TA是Eβ又是空所以再次触发规则3FOLLOW(E’) ⊆ FOLLOW(E’)恒成立。但关键点在于E出现在E → T E中而E是起始符所以$∈ FOLLOW(E)进而$∈ FOLLOW(E’)。同时E也出现在F → ( E )中E后面是)所以)∈ FOLLOW(E)。这就是为什么FOLLOW(E’) { $, ) }。文档里手写的FOLLOW集每一项都应该能回溯到某一条产生式和某一条FOLLOW规则。代码中的computeFollow()函数正是通过反复扫描所有产生式应用这三条规则并用changed标志迭代直到FOLLOW集不再增长。注意FOLLOW集的计算必须在FIRST集完全确定之后才能开始因为规则2和3都依赖于FIRST(β)。代码中main()函数的调用顺序是computeFirst()→computeFollow()→buildPredictTable()这个顺序是铁律颠倒就会得到错误结果。3.3 预测分析表构造从数学定义到内存布局的精确映射预测分析表M[A, a]的定义是若a ∈ FIRST(α)则将A → α放入M[A, a]若ε ∈ FIRST(α)则对每个b ∈ FOLLOW(A)将A → α放入M[A, b]。本项目代码中buildPredictTable()函数完美实现了这一定义for (int i 0; i productions.size(); i) { string lhs productions[i].substr(0, productions[i].find( - )); char A lhs[0]; // 假设LHS是单个字符 string rhs productions[i].substr(productions[i].find( - ) 4); // 情况1a ∈ FIRST(rhs) for (char a : firstSet[rhs[0]]) { // 简化版实际需计算整个rhs的FIRST if (a ! \0) predictTable[A][a] i; } // 情况2ε ∈ FIRST(rhs)则对所有b ∈ FOLLOW(A)填入i if (firstSet[rhs[0]].count(\0)) { // 如果rhs能推出ε for (char b : followSet[A]) { predictTable[A][b] i; } } }这里有两个精妙的设计点1.产生式索引的全局唯一性productionsvector按顺序存储所有产生式i就是其在vector中的下标。这样predictTable[A][a] i不仅记录了该填哪个产生式还提供了O(1)访问产生式内容的能力。当你在调试器里看到predictTable[E][i] 0你立刻就知道要去productions[0]里看E - T E。2.对空产生式的特殊处理firstSet[rhs[0]].count(\0)这个判断是整个表构造的开关。如果一个产生式右部能推出ε比如E - ε那么它的填表逻辑就完全由FOLLOW集驱动。这也是为什么FOLLOW集计算错误会导致整个分析器在遇到$或)时完全失灵——因为该填ε的地方没填查表就返回0程序就报“无匹配产生式”。4. 实操过程与核心环节实现从Code::Blocks工程打开到分析过程全程跟踪4.1 工程导入与一键编译零配置的“开箱即用”资源包中的.cbp文件是Code::Blocks项目的灵魂。它不是一个简单的文本文件而是一个包含了完整构建环境描述的XML。当你双击szzh_compile_exp3.cbpCode::Blocks会自动- 识别项目类型为“Console application”- 加载main.cpp作为主源文件- 读取szh_compile_exp3.depend文件确认main.cpp依赖哪些头文件虽然本项目很简单主要依赖iostream,vector,map等标准库- 应用szh_compile_exp3.layout文件恢复你上次编辑时的窗口布局如代码区、调试器窗口、项目管理器的位置- 设置正确的构建目标为Debug并指定输出目录为bin/Debug/。这意味着你不需要做任何配置不需要手动添加源文件不需要设置包含路径不需要选择编译器默认MinGW。点击工具栏上的“Build and run”按钮或按CtrlF9Code::Blocks就会自动调用g.exe编译main.cpp链接标准库生成可执行文件bin/Debug/szh_compile_exp3.exe然后启动终端运行它。整个过程耗时通常不超过3秒。Debug/和bin/目录已预置是为了让你第一次编译时IDE不会因为找不到输出目录而报错体现了对新手的极致友好。4.2 主程序main.cpp详解一个极简但功能完备的LL(1)引擎main.cpp是整个分析器的心脏全文不到200行但结构清晰分为四个逻辑块1. 全局数据结构初始化约30行mapchar, setchar firstSet, followSet; mapchar, mapchar, int predictTable; vectorstring productions; vectorchar nonTerminals {E, E\, T, T\, F}; // 示例 vectorchar terminals {i, , *, (, ), $};这里定义了所有核心数据结构。nonTerminals和terminals是硬编码的因为实验文法是固定的。这牺牲了一点通用性但换来了绝对的可控性和可调试性。你一眼就能看出所有符号不会因为动态加载而迷失。2. FIRST/FOLLOW集与预测表构建约60行调用computeFirst(),computeFollow(),buildPredictTable()三个函数。这三个函数是纯计算逻辑不涉及I/O是单元测试的最佳目标。你可以单独写一个测试main()只调用computeFirst()输入一个简化文法验证其输出是否符合预期。3. 输入处理与栈初始化约20行string input; cout 请输入待分析的字符串以$结尾: ; getline(cin, input); // 确保以$结尾 if (input.empty() || input.back() ! $) input $; vectorchar stack; stack.push_back($); // 栈底 stack.push_back(E); // 起始符号这里有一个关键细节input必须以$结尾这是LL(1)分析的约定。代码做了容错处理如果用户没输$自动补上。栈的初始化顺序是$在底、E在顶这符合“栈顶是下一个要处理的符号”的约定。4. 核心分析循环约50行这是最精华的部分前面已展示过。它严格遵循算法伪代码每一步都有清晰的cout输出例如栈: [ $, E ] 输入: ii*i$ 匹配 E - T E 栈: [ $, E, T ] 输入: ii*i$ 匹配 T - F T 栈: [ $, E, T, F ] 输入: ii*i$ 匹配 F - i 栈: [ $, E, T, i ] 输入: ii*i$ 匹配成功消耗 i 栈: [ $, E, T ] 输入: i*i$ ...这些输出不是日志而是分析过程的实时快照。它让你看到栈是如何随着推导一步步变化的输入是如何被逐个消耗的。这是理解LL(1)工作原理最直观的教具。4.3 测试用例实战用测试数据.txt覆盖所有边界场景测试数据.txt不是随意罗列的字符串而是精心设计的测试矩阵测试用例类型设计意图预期行为ii*i$合法表达式验证运算符优先级*高于和左结合性完全推导成功最终栈为[$]输入为空(ii)/i$合法嵌套验证括号匹配和/运算符需扩展文法成功但若文法未定义/应报错“无匹配产生式”i*(ii$缺失右括号验证错误定位能力在输入耗尽前栈顶为E或F而输入只剩$此时查表失败报错“期待 ‘)’得到 ‘$’”ii$非法连续运算符验证对非法输入的鲁棒性在第二个处栈顶可能是T或E而不在其FIRST集中报错“期待 ‘i’ 或 ‘(‘得到 ‘’”i$最小合法串验证基础匹配能力快速成功栈从[$, E]变为[$]在Code::Blocks中你可以将测试数据.txt的内容复制粘贴到程序运行的终端里也可以修改main.cpp将getline(cin, input)替换为ifstream ifs(测试数据.txt); getline(ifs, input);实现自动化批量测试。我建议你先手动输入ii*i$一边看输出一边对照文档里的预测分析表亲手“走”一遍推导过程。当你看到stack从[$, E]变成[$, E, T]再变成[$, E, T, F]最后F被i匹配掉你会突然明白所谓“语法分析”不过是根据一张表机械地、确定性地操作一个栈而已。5. 常见问题与排查技巧实录那些文档里不会写但你一定会踩的坑5.1 “程序崩溃了”——最常见的Segmentation Fault来源现象程序运行几秒后弹出“Process returned -1073741819 (0xC0000005)”或直接闪退。根本原因vectorchar stack在stack.back()时为空。排查步骤1. 在Code::Blocks中点击“Debug” → “Start/Continue”或按F8程序会在stack.back()这一行中断。2. 查看stack变量的值发现它是空的{}。3. 回溯代码发现stack.pop_back()被执行了但stack已经为空。解决方案检查主循环的while条件。原始代码是while (!stack.empty() !input.empty())这看起来天衣无缝。但问题出在else分支里当top是非终结符时我们查表并applyProduction()但如果查表失败prodIdx -1我们调用了error()而error()函数内部可能只是cout error endl; exit(1);没有做任何栈保护。更隐蔽的bug是applyProduction()函数里如果产生式右部是ε那么它什么也不往栈里压而stack.pop_back()已经把top弹出了此时如果stack原本只有一个元素比如[$]弹出后就空了下一轮循环stack.back()就崩了。终极修复在applyProduction()里对ε产生式要确保栈不为空才继续。或者更优雅的做法是在error()函数里先打印当前栈和输入状态再exit()这样你至少能看到崩溃前的最后一刻。实操心得永远不要相信vector::back()是安全的。在任何调用前加一句assert(!stack.empty());。Code::Blocks的调试器会立刻告诉你断在哪一行。5.2 “它说‘期待 i得到 ’但我明明输入的是 ii*i”——输入格式陷阱现象输入ii*i程序报错“期待 ‘i’得到 ‘’”。真相你忘了输入结尾的$LL(1)分析器的输入流必须以$结束这是算法的基石。没有$分析器永远不知道“句子结束了”它会一直试图从栈顶符号推导直到栈空或出错。验证方法在main.cpp里找到输入处理部分加一行cout 实际输入: [ input ] endl;。如果你看到输出是[ii*i]那就100%是这个问题。解决方案要么手动输入ii*i$要么让代码自动补全就像资源包里已经做的那样if (input.back() ! $) input $;。这个细节教材里往往一笔带过却是学生调试时耗费最多时间的地方。5.3 “预测分析表里M[E, i]是空的”——文法预处理失败的连锁反应现象调试时predictTable[E][i]的值是0或未定义而你知道它应该填E - T E。根因链-predictTable[E][i]为空 →buildPredictTable()没给它赋值 →firstSet[T]里没有i→computeFirst()没算对T的FIRST集 →T的产生式T - F T没被正确处理 →F的FIRST集没算对 →F - i这条规则被忽略了。排查捷径在Code::Blocks中对computeFirst()函数的第一行设断点然后按F7Step Into单步执行。重点关注firstSet[F]是如何被i填充的。如果这一步就失败了说明productionsvector里根本没有F - i这条规则或者字符串解析有误比如F - i被解析成了F和- i两部分。避坑技巧在main()函数开头加一行for (auto p : productions) cout 产: p endl;确保所有产生式都被正确加载。一个常见的低级错误是.doc文档里的产生式用了全角空格或中文破折号→而代码里用的是半角-导致find( - )失败整个产生式被丢弃。5.4 “为什么i*(ii)能过但i*(ii)少一个)却报错在$上”——错误定位的精度问题现象输入i*(ii$程序在最后报错“期待 ‘)’得到 ‘$’”而不是在缺失括号的那个位置立即报错。原理LL(1)是前瞻一个符号的分析器。它只能看到当前输入符号无法“预知”后面缺什么。当它分析到i*(ii时栈顶可能是E输入是$它查表发现M[E][$]应该是ε因为$∈ FOLLOW(E)于是执行E - ε把E弹出。接着栈顶是F输入还是$查M[F][$]发现没有定义因为F的FOLLOW集里没有$这才报错。错误报告的位置是分析器发现自己无路可走的那个时刻而不是语法错误发生的原始位置。教学启示这恰恰是LL(1)的局限性所在也是为什么现代编译器多用LR系列分析器。但在教学上这个“延迟报错”现象是讲解“FOLLOW集作用”的绝佳案例。你可以修改文法把FOLLOW(E)里的$去掉再测试看看报错位置是否会提前。6. 文档与工程的协同价值为什么《-------------实验3-------------.doc》不是摆设那份命名古怪的Word文档绝不是为了凑数。它的价值在于建立了理论、代码、运行结果三者之间的黄金三角。理论层文档里手写的FIRST/FOLLOW集推导是代码中computeFirst()和computeFollow()函数的数学蓝图。你不能只看代码而不对照文档检查每一步推导是否合理。比如文档里写FOLLOW(T) { , ), $ }那么在调试时你就要去followSet[T]里找这三个字符。如果少了$你就知道computeFollow()的循环迭代次数不够或者某条产生式没被扫描到。代码层文档里的预测分析表是predictTable在内存中的静态快照。当你在调试器里看到predictTable[T][i] 2你立刻翻到文档的表格里找到T行i列看到那里写着T → F T再回头查productions[2]确认它确实是T - F T。这种“文档-内存-代码”的三重印证是消除一切不确定性的唯一方法。运行层文档里对每个测试用例的“分析过程跟踪”是main.cpp中cout输出的预期答案。当你运行ii*i$看到输出的栈变化序列和文档里手写的完全一致那种“啊哈”的顿悟感是任何口头讲解都无法替代的。我曾让一个学生只看文档不看代码手动画出ii*i$的整个分析过程再让他只看代码输出不看文档尝试反向推导出文法。这两个练习比写十遍代码更能建立深刻的理解。这份文档就是一座桥连接了抽象的文法理论和具体的机器执行。7. 个人实操体会从“能跑通”到“真懂”的最后一公里在我用这个包带了三届学生之后最大的体会是“能跑通”和“真懂”之间隔着一个“主动破坏”的距离。学生们第一次运行ii*i$成功会欢呼雀跃。但真正的突破发生在他们开始“搞破坏”的时候。破坏文法把E → T E | ε改成E → T E | T然后运行。你会发现ii*i$在第二个处就报错了。为什么因为现在E能推出T而T的FIRST集是{i, (}不包含所以M[E, ]冲突了。这让你瞬间理解了“提取左公因子”和“消除左递归”不是为了好看而是为了满足LL(1)的数学约束。破坏FIRST集在computeFirst()里故意注释掉对ε的处理。再运行你会发现所有涉及E的推导都乱了套因为M[E, )]和M[E, $]都空了。这让你明白ε不是可有可无的点缀而是FOLLOW集得以生效的钥匙。破坏输入把input $这行删掉然后输入ii*i。程序会无限循环因为栈永远清不完input永远不会空。这让你切肤体会到$作为输入结束符的不可替代性。这些“破坏性实验”成本为零收获巨大。它们把LL(1)从一个被动接受的知识点变成了一个你可以亲手拆解、重组、验证的活物。这个资源包的价值不在于它提供了一个完美的答案而在于它为你提供了一个安全、透明、可逆的实验场。在这里每一次错误都是通往“真懂”的必经之路。当你能自信地解释为什么M[T, *]必须填T → F T而M[T, ]必须填ε时编译原理的大门才算真正为你敞开了一道缝。本文还有配套的精品资源点击获取简介山东科技大学编译原理课程实验三的LL(1)语法分析器完整实现基于C开发直接适配Code::Blocks IDE。包含可一键编译运行的.cbpp工程文件、主程序main.cpp、项目依赖与布局配置以及预置的Debug和bin目录结构。配套Word文档详细说明实验要求、给定文法含E/T/F/G/M等非终结符、FIRST集与FOLLOW集推导过程、预测分析表构造步骤和错误处理逻辑。提供测试数据.txt内含iii、(ii)/i、i(ii)等典型输入串覆盖合法推导、嵌套括号、运算符优先级及非法字符等边界场景支持分析过程跟踪与报错提示。代码已消除左递归、提取左公因子严格满足LL(1)文法判定条件能正确执行栈驱动的匹配、推导模拟、符号回退及语法错误定位。本文还有配套的精品资源点击获取