LL(1)语法分析器完整实现:从文法预处理到分析表构建
发布时间:2026/9/17 21:44:34 作者:尧图编辑部 阅读量:1,286
语法分析器完整实现:从文法预处理到分析表构建)
简介本资源是一份面向计算机专业本科生及编译原理初学者的LL(1)语法分析器实验报告聚焦语法分析核心能力训练解决自顶向下分析中左递归消除、FIRST/FOLLOW集计算、分析表构建与句子识别等关键问题。报告完整呈现C实现全过程含文法输入、预处理、左递归消除、FIRST集与FOLLOW集求解、LL(1)分析表生成及符号串分析函数等核心模块代码与详细说明配套可编辑PDF文档便于学习与复现。资源为单文件PDF格式共1个文件大小256KB内容精炼、结构清晰涵盖实验目的、要求、仪器环境Code::Blocks等、函数设计逻辑与源码节选。目前已有1152人学习下载适合课程实验参考、课程设计支撑及编译原理实践能力提升。1. 这不是一份普通实验报告它是一套可运行的 LL(1) 分析器完整实现链你手头这份《LL(1)语法分析器构造》实验报告远不止是 PDF 里几页排版工整的文字。它内嵌了一套完整、可编译、可调试、可验证的 C 实现逻辑——从文法输入解析、左递归自动检测与消除到 FIRST/FOLLOW 集动态计算、LL(1) 分析表在线构建再到符号串逐步推演的可视化分析过程。这不是“画个分析表交差”的教学作业而是真正走通了编译原理中 top-down 分析全流程的工程化小系统。它解决的核心问题是给定任意符合 LL(1) 条件的算术文法如 E→ET|T, T→T*F|F, F→(E)|i如何让程序自动完成语法合法性判定并输出每一步栈操作与产生式应用的完整轨迹适合正在啃《编译原理》第三版、做吉林大学或哈工大编译原理实验课的学生也适合需要快速复现 LL(1) 构造逻辑的前端编译器初学者或语言工具开发者——你不需要重写算法只需理解这 6 个函数之间的数据流就能把这套逻辑迁移到 Python 或 Java 环境中。2. 文法预处理与左递归消除从原始输入到无冲突产生式集合LL(1) 分析器的前提是文法必须满足无左递归、无公共前缀、FIRST/FOLLOW 不相交。实验报告中的preprocess()和eliminate_1()函数正是这一前提的工程落地。它们不是理论推导的静态结果而是运行时动态重构文法的可执行模块。2.1 文法输入与结构化解析input_grammer()与preprocess()原始文法以字符串形式输入例如E::ET|T。input_grammer()仅负责接收真正的结构化工作由preprocess()完成。该函数承担三项关键任务提取非终结符集U、终结符集u、生成标准化产生式数组P。注意preprocess()中对字符串初始化的处理P[i] 、Uu 并非冗余。Cstd::string若未显式初始化在后续P[i][j]a操作中会因底层 buffer 未分配而崩溃。此处用空格填充是规避operator[]越界访问的常见防御性写法。void preprocess(string *G, string *P, string U, string u, int n, int t, int k) { // ... 初始化 P, U, u ... for (n 0; !G[n].empty(); n) { U[n] G[n][0]; // 提取每个产生式左部首字符作为非终结符 } for (i 0; i n; i) { for (j 4; j G[i].length(); j) { // 跳过 X:: 前4字符 if (U.find(G[i][j]) string::npos u.find(G[i][j]) string::npos) { if (G[i][j] ! | G[i][j] ! ^) { u[t] G[i][j]; // 终结符排除非终结符、|、^ } } } } // 拆分 | 分隔的右部生成独立产生式 for (i 0; i n; i) { k 0; // 重置产生式计数器 for (j 4; j G[i].length(); j) { if (G[i][j] |) { k; // 新产生式 P[k][0] U[i]; P[k][1]:; P[k][2]:; P[k][3]; r 4; } else { P[k][r] G[i][j]; } } k; // 最后一个产生式 } }上述代码将E::ET|T解析为两个独立产生式E::ET和E::T并存入P[0]和P[1]。k即为最终产生式总数。此步骤直接决定了后续 FIRST 集计算的输入粒度——每个P[i]是一个原子单位不可再拆。2.2 左递归检测与重写eliminate_1()的状态机式实现左递归是 LL(1) 的硬性禁忌。eliminate_1()不仅检测更执行标准重写对形如A → Aα | β的规则生成A → βA和A → αA | ε。其核心在于遍历所有以同一非终结符U[i]开头的产生式聚合α左递归部分和β非左递归部分。int eliminate_1(string *G, string *P, string U, string *GG) { int flag 0; // 全局标志是否存在左递归 char C A; // 新增非终结符起始字符 for (int i 0; i 20 U[i] ! ; i) { string arfa , beta ; int flagg 0; // 当前非终结符 U[i] 是否有左递归 for (int j 0; j 100 P[j][0] ! ; j) { if (P[j][0] U[i]) { if (P[j][4] U[i]) { // P[j] 形如 A::A... flagg 1; for (int temp 5; P[j][temp] ! ; temp) { arfa P[j][temp]; // 提取 α } if (P[j1][0] U[i] P[j1][4] U[i]) arfa |; } else { // P[j] 形如 A::β for (int temp 4; P[j][temp] ! ; temp) { beta P[j][temp]; // 提取 β } if (P[j1][0] U[i] P[j1][4] ! U[i]) beta |; } } } if (!flagg) { GG[m] G[i]; // 无左递归原样保留 } else { flag 1; // 重写 A → βA GG[m] string(1, U[i]) ::; if (beta.find(|) ! string::npos) GG[m] ( beta ); else GG[m] beta; while (U.find(C) ! string::npos) C; // 找到新非终结符 C GG[m] string(1, C); m; // 重写 A → αA | ε GG[m] string(1, C) ::; if (arfa.find(|) ! string::npos) GG[m] ( arfa ); else GG[m] arfa; GG[m] string(1, C) |^; m; C; } } return flag; }关键参数说明arfa所有A→Aα中α的拼接含|分隔符如ET|T*Fbeta所有A→β中β的拼接如T|FC动态分配的新非终结符如A,B通过while(U.find(C)!string::npos) C确保不与原U冲突返回值flag供主函数判断是否触发 FOLLOW 集重算实验报告中 FOLLOW 集被硬编码实际应基于新文法重新计算该函数输出GG[]数组即消除左递归后的新文法。后续所有计算FIRST、FOLLOW、分析表均以此为输入而非原始G[]。这是整个流程中唯一一次文法结构变更也是 LL(1) 可行性的基石。3. FIRST/FOLLOW 集计算与 LL(1) 分析表构建从集合推导到二维映射LL(1) 分析器的“智能”源于分析表——一个二维数组table[A][a]其中A是非终结符a是终结符含#。table[A][a]的值即为当栈顶为A、当前输入为a时应选用的产生式。构建此表需两大支柱FIRST 集预测右部首符号与 FOLLOW 集预测右部为空时的后继符号。3.1 FIRST 集的迭代收敛ifempty()与FIRST_X()FIRST 集计算本质是求解方程组FIRST(X){a | X ⇒* a...}∪{ε | X ⇒* ε}。ifempty()先确定哪些非终结符可推导出εFIRST_X()再基于此迭代填充。int* ifempty(string* P, string U, int k, int n) { int* empty new int[n]{0}; // 初始化全0 int flag 1, step 100; while (step-- flag) { flag 0; for (int i 0; i k; i) { int r U.find(P[i][0]); // P[i] 左部非终结符索引 if (P[i][4] ^) { // 直接产生 ε if (!empty[r]) { empty[r] 1; flag 1; } } else { // 检查 P[i] 右部是否全可 ε 推导 bool allEmpty true; for (int j 4; P[i][j] ! ; j) { char c P[i][j]; if (U.find(c) ! string::npos) { if (!empty[U.find(c)]) { allEmpty false; break; } } else break; // 遇到终结符停止 } if (allEmpty P[i][4] ! ) { // 右部非空且全可 ε if (!empty[r]) { empty[r] 1; flag 1; } } } } } return empty; }ifempty()使用迭代法最多 100 步收敛。flag标志本轮是否有更新无更新则收敛。FIRST_X()利用empty[]进行多轮扫描string* FIRST_X(string* P, string U, string u, int* empty, int k, int n) { string* first new string[n]; int step 100; while (step--) { for (int i 0; i k; i) { int r U.find(P[i][0]); if (P[i][4] ^) { if (first[r].find(^) string::npos) first[r] ^; } else { for (int j 4; P[i][j] ! ; j) { char a P[i][j]; if (u.find(a) ! string::npos) { // 终结符 if (first[r].find(a) string::npos) first[r] a; break; } else if (U.find(a) ! string::npos) { // 非终结符 int s U.find(a); for (int tmp 0; first[s][tmp]; tmp) { char ch first[s][tmp]; if (ch ! ^ first[r].find(ch) string::npos) { first[r] ch; } } if (!empty[s]) break; // Y1 不可 ε停止 } } // 若右部全可 ε则加 ^ if (P[i][4] ! /* 右部非空 */ true) { bool allEmpty true; for (int j 4; P[i][j] ! ; j) { char c P[i][j]; if (U.find(c) ! string::npos !empty[U.find(c)]) { allEmpty false; break; } } if (allEmpty first[r].find(^) string::npos) first[r] ^; } } } } return first; }参数逻辑first[r]存储FIRST(U[r])empty[s]表示U[s]是否可 ε 推导u.find(a)判断a是否为终结符。此实现严格遵循定义FIRST(αβ)FIRST(α)若α不 ε∪ (FIRST(β)\{ε})若α可 ε。3.2 FOLLOW 集的硬编码缺陷与修正建议实验报告中FOLLOW被硬编码为string FOLLOW[5] {)#, )#, )#, )#, *)#}。这是严重缺陷——FOLLOW 集必须由文法动态计算而非人工指定。正确做法是对每个产生式A → αBβ将FIRST(β)\{ε}加入FOLLOW(B)若β ⇒* ε则将FOLLOW(A)加入FOLLOW(B)对开始符号S#∈FOLLOW(S)。提示硬编码 FOLLOW 导致分析表错误。例如若文法新增E → idFOLLOW(E)应含#和)但硬编码数组不会更新。生产环境必须实现compute_FOLLOW()函数输入P,U,u,first,empty输出follow[]。3.3 分析表构建create_table()的双重填充逻辑create_table()将 FIRST 和 FOLLOW 映射到二维表。其逻辑分两步FIRST 填充对每个产生式P[i]: A → α对每个a ∈ FIRST(α)设table[A][a] P[i]FOLLOW 填充若ε ∈ FIRST(α)则对每个b ∈ FOLLOW(A)设table[A][b] P[i]string** create_table(string *P, string U, string u, int n, int t, int k, string* first) { string** table new string*[n]; for (int i 0; i n; i) table[i] new string[t1]; for (int i 0; i n; i) for (int j 0; j t1; j) table[i][j] ; for (int i 0; i k; i) { string arfa P[i].substr(4); // 右部 α string fir FIRST(U, u, first, arfa); // 计算 FIRST(α) // FIRST 填充 for (int j 0; j t; j) { if (fir.find(u[j]) ! string::npos) { int p U.find(P[i][0]); table[p][j] P[i]; } } // FOLLOW 填充此处应调用 compute_FOLLOW 得到 follow[] if (fir.find(^) ! string::npos) { // string follow compute_FOLLOW(...)[p]; // 伪代码 string follow FOLLOW[U.find(P[i][0])]; // 报告中硬编码 for (int j 0; j t; j) { if (follow.find(u[j]) ! string::npos) { int p U.find(P[i][0]); table[p][j] P[i]; } } // # 符号填充 int p U.find(P[i][0]); table[p][t] P[i]; // table[A][#] P[i] } } return table; }关键点table[p][t]对应#列索引t因u长度为t#作为第t1个终结符置于末列。table[i][j] 表示ERROR即无对应产生式。4. LL(1) 分析引擎栈驱动的符号串判定与过程可视化analyse()是整个系统的执行中枢。它模拟 LL(1) 分析器的运行时行为维护一个栈stack、读取输入串s依据分析表table进行移进、规约、匹配或报错。其输出不仅是“是/否”更是每一步的详细轨迹这对理解分析过程至关重要。4.1 分析栈与输入流的协同机制LL(1) 分析使用一个栈存储待分析符号初始为#S#为结束符S为开始符号。输入串s末尾也添加#。算法循环执行栈顶x为终结符若x a当前输入则匹配成功弹出x读取下一输入否则报错。栈顶x为非终结符查table[x][a]若为空则报错否则弹出x将table[x][a]右部逆序压栈因栈是 LIFO需逆序保证左→右执行。void analyse(string **table, string U, string u, int t, string s) { string stack #; // 初始化栈 stack U[0]; // 加入开始符号 s #; // 输入串加 # char a s[0]; // 当前输入符号 int i 1; // 栈顶索引stack[i] 为栈顶 int step 1; cout 步骤\t分析栈\t\t余留输入串\t\t所用产生式\n; while (true) { char x stack[i]; // 取栈顶 stack.erase(i, 1); i--; // 弹出 if (u.find(x) ! string::npos) { // x 是终结符 if (x a) { s.erase(0, 1); a s[0]; // 匹配读下一符号 cout step \t stack \t\t s \t\t\n; } else { cout step \t stack \t\t s \t\terror\n; cout s.substr(0, s.length()-1) 不是该文法的句子\n; return; } } else if (x #) { // 栈底与输入底均为 # if (a #) { cout step \t stack \t\t s \t\t成功\n; cout s.substr(0, s.length()-1) 是该文法的句子\n; return; } else { cout step \t stack \t\t s \t\terror\n; cout s.substr(0, s.length()-1) 不是该文法的句子\n; return; } } else { // x 是非终结符 int p U.find(x); int q (a #) ? t : u.find(a); string temp table[p][q]; cout step \t stack \t\t s \t\t temp \n; if (temp ) { cout s.substr(0, s.length()-1) 不是该文法的句子\n; return; } // 将产生式右部逆序压栈 for (int r temp.length()-1; r 3; r--) { if (temp[r] ! ^) { stack temp[r]; i; } } } step; } }逆序压栈详解产生式E::ET存于temp右部为ET。for(rtemp.length()-1; r3; r--)从T→→E依次压栈使栈中顺序为E, , T栈顶在右从而保证规约时按E → ET的自然顺序展开。4.2 关键状态与错误捕获点非法符号检测analyse()开头遍历s若u.find(s[i]) string::npos立即输出error。这是词法层面的校验确保输入只含文法定义的终结符。栈空错误若栈顶x为非终结符但table[p][q] 表示分析表无定义属语法错误如ii*i。匹配失败终结符栈顶x与当前输入a不等属语法错误。结束条件仅当stack # s #时成功任何其他栈/输入组合均失败。此函数输出格式严格对齐实验要求“步骤、分析栈、余留输入串、所用产生式”可直接用于实验报告截图或自动化测试比对。5. 实战验证与边界案例调试用ii*i和ii*i拆解分析过程理论与代码终需实证。我们以实验报告指定的两个典型输入——合法句ii*i与非法句ii*i——驱动analyse()观察其内部状态流定位潜在陷阱。5.1 合法句ii*i的 12 步完整推演假设文法已消除左递归E→TE,E→TE|ε,T→FT,T→*FT|ε,F→(E)|iFOLLOW(E) {),#},FOLLOW(T) {,),#}。分析ii*i的关键步骤如下步骤分析栈余留输入串所用产生式说明1#Eii*i#E→TE栈顶E输入i查表得E→TE2#ETii*i#T→FT栈顶T输入i查表得T→FT3#ETFii*i#F→i栈顶F输入i查表得F→i4#ETi*i#—F弹出i匹配输入移至5#ETi*i#T→ε栈顶T输入∈FOLLOW(T)选ε规约6#Ei*i#E→TE栈顶E输入查表得E→TE............后续步骤继续展开TE验证要点步骤 4 中F→i执行后栈顶变为T输入为。此时T的FOLLOW含故步骤 5 选用T→ε而非报错。这体现了 FOLLOW 集在 ε 产生式选择中的决定性作用。5.2 非法句ii*i的错误定位与修复路径输入ii*i时analyse()在步骤 2 即报错步骤分析栈余留输入串所用产生式说明1#Eii*i#E→TE正常2#ETii*i#T→FT正常3#ETFii*i#F→ii匹配输入移至第二个i4#ETi*i#error栈顶T输入iFIRST(T) {*}i ∉ FIRST(T)且i ∉ FOLLOW(T)表项为空错误根源T的FIRST为{*}FOLLOW为{,),#}i不在其中。这表明i无法跟在T后即ii不是合法前缀。analyse()在步骤 4 输出error并终止精准定位到第二个i。调试技巧若遇意外error检查三处①preprocess()是否正确提取终结符i是否在u中②eliminate_1()是否彻底消除左递归残留左递归会导致 FIRST 计算错误③create_table()中FOLLOW是否与当前文法一致硬编码 FOLLOW 是最大隐患。5.3 从 C 到 Java/Python 的迁移要点本实现虽用 C但核心逻辑可无缝迁移到其他语言Java用ArrayListString替代string* PHashMapCharacter, String存tableStringBuilder处理字符串。Python用list存P和Udict存tablecollections.deque作栈append()/pop()。关键差异C 的string::find()在 Python 中为str.find()或in操作Java 中为String.indexOf()。FIRST计算中的迭代收敛逻辑完全一致无需修改。此报告的价值正在于它提供了一条从理论定义LL(1) 条件到可执行代码6 个函数再到可验证输出步骤日志的完整闭环。你不必从零推导只需读懂preprocess如何切分产生式、eliminate_1如何重写规则、analyse如何驱动栈——然后把它跑起来。本文还有配套的精品资源点击获取