VC++实现的IF-ELSE语句LL(1)语法分析与四元式生成工程
本文还有配套的精品资源,点击获取
简介:一套可直接编译运行的Visual C++ 2008工程,专注处理IF-ELSE条件语句的语法分析和中间代码翻译。采用LL(1)文法进行自顶向下分析,能准确识别嵌套IF-ELSE结构、布尔表达式及分支语句体,并输出标准四元式序列,如(jnz, A,, p)、(j,, _, q)等。核心逻辑集中在Compare.cpp中,线性表工具封装在LinearList.h里,测试样例存于compare.txt。工程包含完整VS2008项目文件(.sln、.vcproj)、调试目录(Debug)、资源文件(.rc、.res)及编译产物(.obj、.exe、.pdb等),支持开箱即用。运行后输出清晰的分析过程与四元式结果,便于验证LL(1)预测分析表构造、FIRST/FOLLOW集计算、语法树构建及跳转地址回填等关键步骤。适用于高校编译原理课程实验、LL(1)文法实践、中间代码生成教学演示及学生课程设计参考。
1. 项目概述:这不是一个“玩具编译器”,而是一套可验证的LL(1)教学闭环系统
你手头拿到的这个VC++工程,名字叫Compare,但它干的活远不止“比较”那么简单。它是一个面向教学验证的、端到端可追踪的IF-ELSE语句翻译系统——从一行符合文法的if (a > b) { x = 1; } else { y = 2; }输入开始,到最终输出一串带跳转地址的四元式序列(比如(jnz, a>b, _, 105)、(j, _, _, 108)、(=, 1, _, x)),整个过程每一步都可观察、可打断、可对照理论推导。我带过七届编译原理实验课,见过太多学生把LL(1)预测分析表背得滚瓜烂熟,却在调试时对着synch符号发呆,或者搞不清为什么else要归入FOLLOW(Stmt)而不是FIRST(ElsePart)。这个工程就是为解决这类“纸上谈兵”痛点而生的:它不追求工业级语法覆盖,但把IF-ELSE这一经典控制结构的词法切分→FIRST/FOLLOW计算→预测分析表构造→递归下降模拟→语义动作嵌入→四元式生成→地址回填这整条链路,用不到800行C++代码扎扎实实跑通了。关键词里写的“IF-ELSE翻译”“LL(1)分析”“四元式生成”,不是功能罗列,而是它真正落地的三个锚点——你改一行测试用例,就能立刻看到语法树节点如何生长、预测栈如何变化、四元式地址如何从占位符_被回填为真实标号。它用Visual C++ 2008实现,不是怀旧,而是因为VC90的调试器能清晰显示std::stack的实时状态、LinearList中指针偏移量、甚至goto跳转前后的EIP寄存器值,这对理解自顶向下分析中“预测—匹配—展开”的时序关系至关重要。如果你正在做课程设计、准备实验报告,或者想亲手验证教材上那个抽象的LL(1)表到底怎么驱动分析器,那么这个工程不是参考代码,而是你的可交互教具。
2. 整体设计与思路拆解:为什么只做IF-ELSE?为什么坚持LL(1)?为什么用线性表而非STL?
2.1 聚焦IF-ELSE:教学场景下的精准减法
很多人第一反应是:“只支持IF-ELSE?太窄了吧?”恰恰相反,这是经过反复教学验证的最优复杂度切口。IF-ELSE结构天然包含LL(1)分析中最棘手的三大挑战:
-左递归规避需求:Stmt → if Expr then Stmt else Stmt | ...若直接写,会产生直接左递归,必须改写为Stmt → if Expr then Stmt ElsePart,而ElsePart → else Stmt | ε又引出空产生式的处理;
-FOLLOW集冲突显性化:else关键字既可能紧跟在then后(then Stmt else),也可能出现在语句末尾(Stmt else),其FOLLOW集必须精确计算为{else, $},稍有偏差就会导致预测表多行冲突;
-语义动作时机敏感:四元式生成不能等到整个if语句分析完才输出,必须在then后立即生成jnz跳转,在else前生成j无条件跳转,并在分支体结束时回填目标地址——这要求语义动作必须嵌入在文法产生式右部的特定位置,而非笼统挂载在非终结符上。
这个工程没有试图支持while或函数调用,就是为了把上述三点掰开揉碎:你在Compare.cpp里能看到parse_IfStmt()函数中,match(TOKEN_IF)后立刻调用genQuad("jnz", exprTemp, "", ""),紧接着match(TOKEN_THEN)后调用backPatch(nextQuad(), quadList.size()),这种“边分析边生成”的节奏,正是编译原理中“语法分析驱动语义分析”的最直观体现。教学价值不在于广度,而在于你能指着某一行代码说:“看,这里backPatch的调用,就是在模拟教材图3.27里那个‘回填地址链表’的操作。”
2.2 死守LL(1):拒绝递归下降的“黑盒感”,暴露预测逻辑
工程明确采用LL(1)文法,而非更灵活的递归下降(Recursive Descent)或LR(1)。这不是技术保守,而是刻意暴露预测机制。LL(1)要求文法满足两个硬约束:无左递归、无公共左因子,且对每个非终结符A和输入符号a,预测分析表M[A,a]至多一个产生式。这个工程的文法定义(隐含在parse_*函数调用逻辑中)严格遵循此规则:
-Stmt → if Expr then Stmt ElsePart | ID = Expr ; | ...中,if和ID的FIRST集不相交,消除了公共左因子;
-ElsePart → else Stmt | ε的处理,强制要求else必须属于FOLLOW(ElsePart),而FOLLOW(ElsePart) = FOLLOW(Stmt) = {else, $},因此当输入为else时,必须选择else Stmt,当输入为$(文件结尾)时,必须选择ε。
你在调试时单步进入predict()函数(虽然源码中未显式命名,但逻辑内嵌于switch分支判断),会发现它本质就是一个查表过程:根据当前栈顶非终结符(如Stmt)和当前输入记号(如TOKEN_ELSE),查predictTable[Stmt][TOKEN_ELSE]得到应展开的产生式。这种“查表—展开—匹配”的机械感,恰恰是理解LL(1)预测分析器工作原理的核心。如果用纯递归下降,parse_Stmt()里直接if (lookahead == TOKEN_IF) parse_IfStmt(); else if (lookahead == TOKEN_ID) parse_AssignStmt();,学生就看不到预测表如何消除歧义,也体会不到synch符号在错误恢复中的作用——而本工程在parse_Stmt()末尾的else分支里,明确写了error("Unexpected token: " + tokenName(lookahead)); syncToFollowSet(Stmt);,syncToFollowSet函数会丢弃输入直到遇到{else, $}中的符号,这就是LL(1)错误恢复的教科书实现。
2.3 线性表替代STL:可控内存与教学透明性
工程将线性表工具封装在LinearList.h中,而非使用std::vector或std::list。这不是排斥现代C++,而是教学可控性的必然选择。LinearList<T>是一个固定大小的数组模拟链表,其核心成员是T* data和int size,所有操作(insertAt,deleteAt,getAt)都通过下标运算显式完成。这意味着:
- 在VC++2008调试器中,你可以直接展开quadList对象,看到data[0]到data[quadList.size()-1]每一项的完整四元式结构(op,arg1,arg2,result),无需进入STL复杂的迭代器模板;
- 地址回填逻辑backPatch(target, listIndex)中,listIndex直接对应quadList.data[listIndex].result字段,学生能清晰看到“占位符_是如何被替换为具体标号”的物理过程;
- 当你需要演示“四元式链表增长导致内存重分配”的边界情况时,只需修改LinearList的初始容量(如从100改为5),就能触发realloc并观察data指针变化,这种底层细节在STL容器中是被完全封装的。
我曾让学生对比两版代码:一版用std::vector<Quad>,一版用LinearList<Quad>,然后问他们“quadList.size()在genQuad调用后是否等于新生成四元式的标号?”。用STL版的学生往往答“应该是”,而用LinearList版的学生能指着调试窗口说:“size是5,所以新四元式标号是5,因为标号从0开始计数”。这种对数据结构与逻辑编号之间映射关系的直觉,是高级容器无法提供的。
3. 核心细节解析与实操要点:从文法定义到四元式生成的全链路拆解
3.1 文法定义与FIRST/FOLLOW集的手动推导(这才是关键!)
工程虽未提供.yacc文件,但其隐含文法完全可逆向还原。核心产生式如下(已消除左递归与左因子):
Program → StmtList $ StmtList → Stmt StmtList | ε Stmt → IfStmt | AssignStmt | ... IfStmt → if Expr then Stmt ElsePart ElsePart → else Stmt | ε Expr → SimpleExpr RelOp SimpleExpr | SimpleExpr SimpleExpr → Term { AddOp Term } Term → Factor { MulOp Factor } Factor → ID | NUM | ( Expr ) RelOp → > | < | == | != AddOp → + | - MulOp → * | /现在重点来了:FIRST和FOLLOW集不是凭空来的,必须手动推导并硬编码进预测逻辑。以ElsePart为例:
-FIRST(ElsePart)=FIRST(else Stmt) ∪ FIRST(ε)={else} ∪ {ε}={else, ε};
-FOLLOW(ElsePart)=FOLLOW(Stmt)(因IfStmt → ... ElsePart,且ElsePart在产生式右部末尾);
-FOLLOW(Stmt)=FOLLOW(IfStmt) ∪ FOLLOW(AssignStmt) ∪ FOLLOW(StmtList)(因StmtList → Stmt StmtList,Stmt后跟StmtList,故FOLLOW(Stmt) ⊇ FIRST(StmtList);又因StmtList → ε,故FOLLOW(Stmt) ⊇ FOLLOW(StmtList));
- 最终FOLLOW(Stmt) = {else, $}(StmtList在Program末尾,$是结束符;else来自IfStmt中then Stmt else的else)。
这个推导结果直接决定了parse_ElsePart()的分支逻辑:
void parse_ElsePart() { if (lookahead == TOKEN_ELSE) { match(TOKEN_ELSE); parse_Stmt(); // 生成(j, _, _, nextQuad()) 四元式,跳过else分支 genQuad("j", "", "", ""); } // else分支隐含ε产生式,无需操作 }注意:此处没有else if (lookahead == TOKEN_$)的显式判断,因为$是文件结束符,在parse_StmtList()循环中自然处理。这种“用控制流代替显式空产生式匹配”的写法,是手工LL(1)实现的典型技巧——它把FOLLOW集的判断逻辑下沉到了调用方(parse_IfStmt()中parse_ElsePart()之后的代码位置),而非在parse_ElsePart()内部查表。你在Compare.cpp第142行能看到parse_IfStmt()末尾的注释:“// ElsePart may be ε, so no action needed here”,这就是对FOLLOW(ElsePart)={else,$}的代码化确认。
3.2 四元式生成的三阶段模型:生成→暂存→回填
四元式生成不是简单打印,而是一个带状态管理的三阶段流水线:
1.生成阶段(Gen):当语法分析到达语义动作点(如then后),调用genQuad(op, arg1, arg2, result),将四元式追加到quadList末尾,并返回其标号(即quadList.size()-1)。此时result字段常为""(占位符),如genQuad("jnz", exprTemp, "", "");
2.暂存阶段(Save):为后续回填准备,需保存待填地址的位置。工程用LinearList<int> nextList存储“下一个四元式标号”,用LinearList<int> quadList存储四元式本身。当生成jnz时,同时调用makelist(quadList.size()-1)创建一个仅含当前标号的列表;
3.回填阶段(Backpatch):当分析到达分支体结束点(如}或下一个else),调用backPatch(list, target),遍历list中每个标号,将quadList.data[i].result设为target。例如,then分支体结束后,backPatch(thenNextList, nextQuad())将所有jnz的result填为then体之后的标号。
这个模型的关键在于nextList的管理。Compare.cpp中parse_Stmt()函数开头有LinearList<int> stmtNextList;,这是局部变量,确保每个语句块的回填链相互隔离。当你调试嵌套if (a) if (b) x=1; else y=2;时,外层if的thenNextList和内层if的thenNextList是两个独立对象,不会互相污染——这正是手工管理比自动垃圾回收更适合教学的原因:你能亲眼看到内存中两个链表如何并存。
3.3 VC++2008工程配置的隐藏细节:为什么必须用Debug模式?
工程目录里的Debug文件夹不是摆设。VC++2008的默认Release配置会开启“全程序优化”(/GL),导致:
- 函数内联使parse_*调用栈扁平化,单步调试时无法观察递归展开层次;
- 变量寄存器优化使lookahead、quadList.size()等关键变量在调试窗口中显示为<optimized out>;
-LinearList的data指针可能被优化为直接访问内存,失去数组下标可视化。
因此,必须确保在VS2008中以Debug配置运行。具体操作:菜单栏Build → Configuration Manager,将Active solution configuration设为Debug;再检查项目属性Configuration Properties → C/C++ → Optimization,确认Optimization为Disabled (/Od)。此时调试器才能完整显示:
-quadList.data数组的每一项内容(右键quadList→QuickWatch→ 输入quadList.data[0]);
-nextList.data中存储的标号链(如nextList.data[0]=3, nextList.data[1]=5);
-lookahead变量实时值(如TOKEN_ELSE)。
我在课堂上演示时,会让学生把断点打在genQuad调用前,然后观察quadList.size()从0涨到1、2、3…,再打在backPatch调用后,看quadList.data[2].result如何从""变成"6"——这种“数值变化可视化”,是理解中间代码生成动态过程的黄金时刻。
4. 实操过程与核心环节实现:从编译到运行的逐帧解析
4.1 编译与环境准备:零依赖,但需注意字符编码
工程使用VC++2008开发,但无需安装完整VS2008。你只需:
- 下载并安装Microsoft Visual C++ 2008 Express Edition(免费);
- 或使用更高版本VS(如VS2019),打开.sln文件后,VS会自动提示升级项目格式,点击“确定”即可(升级后仍能正常编译,因代码无VC90特有API)。
关键注意事项是测试文件compare.txt的编码。该文件必须保存为ANSI编码(即Windows-1252),而非UTF-8。原因:VC++2008默认用MultiByteToWideChar(CP_ACP, ...)读取文件,若compare.txt是UTF-8,中文注释或特殊符号会乱码,导致词法分析器将>识别为非法字符。验证方法:用记事本打开compare.txt,点击文件 → 另存为,在底部“编码”下拉框中确认为ANSI。若已误存为UTF-8,可用Notepad++转换:编码 → 转为ANSI,再保存。我在第一次部署时就因这个细节卡了半小时——compare.txt里一行if (x > 0)被读成if (x ï¼ 0),ï¼根本不在词法分析器的符号表中。
4.2 源码核心逻辑精读:Compare.cpp的12个关键段落
Compare.cpp是心脏,全文约750行。以下是必须精读的12个段落(行号基于原始工程,可能因换行微调):
| 行号范围 | 功能 | 教学价值 |
|---|---|---|
| 1-45 | 全局变量声明:LinearList<Quad> quadList,LinearList<int> nextList,int lookahead,char* inputBuffer | 理解全局状态如何支撑LL(1)分析器的“无回溯”特性;inputBuffer是手动实现的缓冲区,非std::string |
| 46-88 | getToken()函数:手动扫描inputBuffer,返回TOKEN_IF,TOKEN_ID等 | 词法分析器极简实现,switch(*p)直接匹配字符,p++推进指针,体现“一次读取一个记号”的LL(1)前提 |
| 89-120 | predictTable模拟逻辑:parse_Stmt()中switch(lookahead)分支 | 预测分析表的代码化呈现,每个case对应表中一行,default即synch错误恢复 |
| 121-155 | parse_IfStmt():match(TOKEN_IF)→parse_Expr()→match(TOKEN_THEN)→parse_Stmt()→parse_ElsePart() | IF-ELSE文法的完整展开链,注意parse_ElsePart()调用后无match,体现ε产生式 |
| 156-180 | parse_Expr()与parse_SimpleExpr()的嵌套调用 | 展示算术表达式优先级如何通过文法嵌套实现:Expr调用SimpleExpr,SimpleExpr调用Term,Term调用Factor |
| 181-210 | genQuad()与backPatch()实现:quadList.insertAt(quadList.size(), quad),for(int i=0; i<list.size(); i++) quadList.data[list.data[i]].result = target | 四元式生成与回填的物理操作,insertAt的size()参数即标号,list.data[i]即待填地址索引 |
| 211-235 | makelist()与mergeList():LinearList<int> l; l.insertAt(0, quadNum); return l; | 构建回填链表的基础操作,“合并两个链表”即for循环拼接,无STL的splice魔法 |
| 236-260 | parse_StmtList()循环:while(lookahead != TOKEN_DOLLAR) | Program → StmtList $的实现,TOKEN_DOLLAR即$,体现LL(1)分析器以结束符为终止信号 |
| 261-285 | main()函数:loadFile("compare.txt")→getToken()初始化→parse_StmtList()→printQuads() | 入口逻辑清晰,loadFile将整个文件读入inputBuffer,避免流式读取的复杂性 |
| 286-310 | printQuads():格式化输出(op, arg1, arg2, result),result为空时打印_ | 输出规范严格遵循四元式标准,_占位符直观展示回填前状态 |
| 311-335 | 错误处理error()与syncToFollowSet():printf("Error at line %d\n", lineNo),while(!isInFollowSet(lookahead, set)) getToken() | synch符号的实战应用,isInFollowSet查硬编码的FOLLOW集合(如{TOKEN_ELSE, TOKEN_DOLLAR}) |
| 336-350 | LinearList辅助函数:isEmpty(),getSize(),clear() | 手工容器的完备性,clear()释放data内存,体现资源管理意识 |
特别提醒:第142行// ElsePart may be ε和第225行// backPatch the jnz to jump over else part这两处注释,是作者留下的“教学路标”,务必结合上下文代码理解其含义。
4.3 测试用例compare.txt深度解析:五个层级的验证
compare.txt不是随意写的例子,而是按教学梯度设计的五层验证集:
| 层级 | 测试用例 | 验证目标 | 预期四元式关键点 |
|---|---|---|---|
| 基础层 | if (a > b) x = 1; | 单分支if,无else | (jnz, a>b, _, 3),(=, 1, _, x),(j, _, _, _)—— 注意末尾j的result为_,因无else需跳过整个if |
| 嵌套层 | if (a) if (b) x=1; else y=2; | else归属问题 | 外层jnz跳转目标应为y=2之后,内层jnz跳转目标应为y=2之前;else必须绑定到最近的if |
| 空分支层 | if (a) ; else x=1; | ε产生式处理 | then后生成jnz,else前生成j,then体为空,故jnz的result直接填j的标号 |
| 表达式层 | if (a + b * c == d) x = y + z; | 算术表达式优先级与关系运算 | SimpleExpr先生成(*, b, c, t1),(+, a, t1, t2),再==生成(==, t2, d, t3),最后jnz t3 |
| 错误恢复层 | if (a) x=1; elsx y=2; | synch机制 | elsx非法,syncToFollowSet(ElsePart)丢弃直到else或$,应跳过elsx并继续分析y=2 |
运行时,观察控制台输出的四元式序列,对照上表检查标号连续性、jnz与j的配对、_占位符的回填位置。例如基础层输出中,若第三个四元式是(j, _, _, 5)而非(j, _, _, _),说明backPatch逻辑有误——这正是调试的起点。
5. 常见问题与排查技巧实录:那些年我们踩过的坑
5.1 “四元式标号不连续”问题:根源在quadList.size()的时序
现象:输出四元式标号跳跃,如0:(jnz,...),1:(=,...),3:(j,...),缺少2。
根因:genQuad()中quadList.insertAt(quadList.size(), quad)调用前,quadList.size()已被其他函数(如makelist())意外修改。
排查步骤:
1. 在genQuad()开头加断点,观察quadList.size()值;
2. 单步执行,确认insertAt参数是否等于预期标号;
3. 检查makelist()是否误用了quadList.size()(正确应为quadList.size()-1)。
修复方案:genQuad()中显式记录标号:
int quadNum = quadList.size(); quadList.insertAt(quadNum, quad); return quadNum; // 返回标号供backPatch使用这是手工LL(1)实现中最易错的细节——标号必须是insert前的size,而非insert后的size。
5.2 “else总是被忽略”问题:FOLLOW集计算偏差
现象:if (a) x=1; else y=2;中,y=2不被执行,四元式无j跳转。
根因:parse_ElsePart()的if (lookahead == TOKEN_ELSE)判断失败,因lookahead未正确更新到else。
排查步骤:
1. 在parse_IfStmt()中match(TOKEN_THEN)后设断点,观察lookahead值;
2. 单步执行parse_Stmt()(then后的分支体),确认其结束时lookahead是否为TOKEN_ELSE;
3. 检查parse_Stmt()末尾是否有getToken()调用遗漏(应在match后立即更新lookahead)。
修复方案:确保每个match(token)后紧跟lookahead = getToken(),并在parse_StmtList()循环中统一维护lookahead。教材常省略此细节,但手工实现必须显式。
5.3 “中文注释导致崩溃”问题:字符编码与缓冲区溢出
现象:compare.txt含中文注释(如// 初始化变量)时,程序崩溃或输出乱码四元式。
根因:ANSI编码下中文占2字节,getToken()按单字节扫描,将//后的中文首字节误判为运算符。
排查步骤:
1. 用十六进制编辑器打开compare.txt,查看中文“初”字的编码(如B3 F5);
2. 在getToken()中switch(*p)处设断点,观察*p值是否为0xB3(非法字符);
3. 检查inputBuffer分配大小是否足够(compare.txt长度×2)。
修复方案:
-教学推荐:删除所有中文注释,用英文(// init variables);
-进阶方案:修改getToken(),添加UTF-8多字节检测(if ((*p & 0xC0) == 0xC0) { /* skip UTF-8 char */ }),但这超出LL(1)教学范围。
5.4 “Debug目录缺失”问题:VC++2008的输出路径陷阱
现象:编译成功但找不到Compare.exe,双击sln文件报错“无法启动程序”。
根因:VC++2008默认输出路径为.\Debug\,但工程文件中Output Directory可能被误设为绝对路径(如C:\old\path\Debug\),而该路径不存在。
排查步骤:
1. 右键项目 →Properties→Configuration Properties → General;
2. 查看Output Directory,确认为$(SolutionDir)$(Configuration)\(相对路径);
3. 查看Target Name,确认为Compare。
修复方案:将Output Directory改为$(SolutionDir)Debug\,Intermediate Directory改为$(SolutionDir)Debug\,然后清理解决方案(Build → Clean Solution)再重新生成。
5.5 “四元式地址回填为0”问题:nextList生命周期管理失误
现象:jnz的result字段回填为0而非正确标号。
根因:nextList作为局部变量,在parse_Stmt()返回后被销毁,但backPatch尝试访问已释放内存。
排查步骤:
1. 在parse_Stmt()开头声明LinearList<int> stmtNextList;;
2. 在backPatch(stmtNextList, target)调用后,观察stmtNextList.size()是否为0(应为0,因已清空);
3. 检查backPatch函数内是否对list.data[i]做了越界访问(i >= list.size())。
修复方案:backPatch函数内添加边界检查:
void backPatch(LinearList<int>& list, const char* target) { for(int i = 0; i < list.size(); i++) { if (list.data[i] < quadList.size()) { // 防越界 quadList.data[list.data[i]].result = target; } } list.clear(); // 清空链表,避免重复回填 }这是手工内存管理的必修课——STL容器会自动处理,但LinearList需要你亲手加固。
6. 工程扩展与教学延伸:从IF-ELSE到更广阔的编译世界
这个工程的价值不仅在于它“是什么”,更在于它“可以成为什么”。我指导学生做的三个经典延伸项目,都基于此代码基线:
6.1 扩展为WHILE循环翻译器
只需在文法中增加:
Stmt → WhileStmt | IfStmt | AssignStmt WhileStmt → while ( Expr ) Stmt对应修改:
-parse_Stmt()中添加case TOKEN_WHILE:分支;
-parse_WhileStmt()中:match(TOKEN_WHILE)→match(TOKEN_LPAREN)→parse_Expr()→match(TOKEN_RPAREN)→genQuad("jnz", exprTemp, "", "")生成循环条件跳转;
- 关键技巧:while的四元式需“先跳转后执行”,因此在parse_Expr()后立即genQuad("jnz"),并将该标号存入whileBeginList;Stmt分析完后,backPatch(whileBeginList, whileStartLabel)回填到循环头。
6.2 集成符号表管理
在LinearList.h旁新增SymbolTable.h,定义:
struct Symbol { char name[32]; int type; int offset; }; LinearList<Symbol> symTable;在parse_AssignStmt()中,match(TOKEN_ID)后调用enterSymbol(tokenValue),在genQuad("=", ...)前调用lookupSymbol(tokenValue)获取偏移量。这样,x = y + 1就能生成(+, y_addr, 1, t1)而非(+, y, 1, t1),迈出从“字符串替换”到“地址计算”的关键一步。
6.3 可视化语法树生成
修改parse_*函数,为每个非终结符创建TreeNode:
struct TreeNode { char* label; TreeNode* child; TreeNode* sibling; }; TreeNode* parse_IfStmt() { TreeNode* root = new TreeNode{"IfStmt", nullptr, nullptr}; root->child = parse_Expr(); // 子节点 root->child->sibling = parse_Stmt(); // 兄弟节点 return root; }最后调用printTree(root)输出缩进树状图。这能让学生直观看到IfStmt节点下挂载Expr和Stmt子树,理解语法分析如何构建AST。
这些扩展都不需要重写核心LL(1)框架,只需在现有parse_*骨架上“插件式”添加。这正是优秀教学工程的设计哲学:它不试图包罗万象,而是为你搭好第一个稳固的脚手架,让你能自信地向上搭建自己的编译器高楼。当你在Compare.cpp里亲手把else分支的j四元式标号从_改成105,那一刻,你不再是在背诵LL(1)的定义,而是在用代码重演图灵机的每一次状态跃迁——这,才是编译原理最本真的魅力。
本文还有配套的精品资源,点击获取
简介:一套可直接编译运行的Visual C++ 2008工程,专注处理IF-ELSE条件语句的语法分析和中间代码翻译。采用LL(1)文法进行自顶向下分析,能准确识别嵌套IF-ELSE结构、布尔表达式及分支语句体,并输出标准四元式序列,如(jnz, A,, p)、(j,, _, q)等。核心逻辑集中在Compare.cpp中,线性表工具封装在LinearList.h里,测试样例存于compare.txt。工程包含完整VS2008项目文件(.sln、.vcproj)、调试目录(Debug)、资源文件(.rc、.res)及编译产物(.obj、.exe、.pdb等),支持开箱即用。运行后输出清晰的分析过程与四元式结果,便于验证LL(1)预测分析表构造、FIRST/FOLLOW集计算、语法树构建及跳转地址回填等关键步骤。适用于高校编译原理课程实验、LL(1)文法实践、中间代码生成教学演示及学生课程设计参考。
本文还有配套的精品资源,点击获取
