简介:本资源为天津理工大学编译原理实验三的完整实验报告,面向计算机相关专业正在学习编译原理课程的学生,聚焦语义分析与中间代码生成环节。报告以文法G[E]为对象,要求从LL1分析法、算符优先分析法或LR分析法中择一,构造属性文法描述,并在实验二语法分析的基础上完成语法制导翻译程序设计,最终输出与测试用例等价的四元式中间代码序列。压缩包内为1个doc文档,约381KB,内容涵盖实验内容、目的、要求、过程记录、结果与结论,并附有源程序代码,涉及variable_T结构体、char_stack结构体及二维分析表等实现细节,可帮助读者理解语义动作设计与异常错误处理思路。目前已有376人学习下载,适合需要参考实验报告结构、对照四元式生成流程与排错思路的同学使用。
1. 从语法树到四元式:一份能跑通的语义分析实验拆解
很多同学做完语法分析就卡住了——状态栈能跑、符号栈能跑,但一到语义动作就不知道往哪塞。天津理工大学编译原理实验3要解决的就是这个断层:在实验二 LR 分析器的基础上,把语法制导翻译嵌进去,让每次规约不仅完成语法归约,还顺手吐出一条四元式。文法 G[E] 覆盖了加减乘除、幂运算和括号嵌套,最终输出形如(+, a, b, t1)的中间代码序列。这份资源适合正在做编译原理实验、需要一份可对照的 LR 语义分析实现参考的读者,尤其是选了 LR 分析法但卡在语义动作挂载位置的同学。代码用 C++ 写成,核心思路是把符号栈升级为带endchar字段的结构,规约时从栈中取出操作数对应的临时变量名,拼成四元式后压回新变量。
2. LR 语义分析器怎么搭:符号栈改造与四元式生成
2.1 为什么选 LR 而不是 LL 或算符优先
实验允许在 LL1、算符优先、LR 三者中选一个。LL1 需要消除左递归,而原文法E→E+T | E-T | T是典型的左递归结构,改写后属性传递会变复杂;算符优先虽然直观,但对^的右结合性和括号嵌套处理起来容易翻车。LR 分析法的优势在于:分析表驱动,规约时机明确,语义动作天然挂在规约产生式上。每次执行r1到r10的规约时,栈顶恰好就是该产生式右部对应的符号和状态,操作数信息可以直接从符号栈中读取,不需要额外的属性栈传递。这也是为什么实验二如果已经用 LR 跑通了语法分析,实验三的改动量其实集中在符号栈结构扩展和规约分支里加四元式输出。
2.2 符号栈的改造:从 char 到带 endchar 的结构体
原始 LR 分析器的符号栈只存字符,规约时知道“这里有个 E”,但不知道“这个 E 对应哪个临时变量”。解决办法是给符号栈每个元素加一个endchar字段,记录该非终结符当前代表的中间变量名。原文代码里char_stack结构体就是这么设计的:
typedef struct char_stack { char content; // 当前字符,如 'E'、'T'、'F'、'P'、'i' string endchar; // 该符号代表的中间变量,如 "i"、"t1"、"t2" int num; // 中间变量序号 } char_stack;content用于查分析表、判断规约产生式;endchar用于语义动作取操作数。当词法分析扫到标识符i时,endchar初始化为"i";每次规约生成新临时变量t1、t2后,把新变量名写入规约后非终结符的endchar。这样到上层规约时,直接从栈里读endchar就能拿到操作数,不用再维护独立的语义栈。
2.3 四元式生成的核心逻辑:以 r1 和 r4 为例
四元式结构体定义很简单,四个字段分别对应操作符、左操作数、右操作数、结果变量:
typedef struct variable_T { char operate; // 操作符:+ - * / ^ string var1; // 左操作数 string var2; // 右操作数 int num; // 结果变量编号,对应 t{num} } variable_T; variable_T t[size]; int tsize = -1; // 当前已生成的临时变量数以r1(E→E+T)为例,规约发生时符号栈顶三个元素依次是E、+、T。代码先取T的endchar作为右操作数,指针回退两格取E的endchar作为左操作数,然后生成新临时变量:
else if (production == "r1") { int po = (*pointer); string st = stack[po].endchar; // T 对应的变量,右操作数 po -= 2; string se = stack[po].endchar; // E 对应的变量,左操作数 tsize++; t[tsize].num = tsize + 1; t[tsize].operate = '+'; t[tsize].var1 = se; t[tsize].var2 = st; cout << "\t(" << t[tsize].operate << "," << t[tsize].var1 << "," << t[tsize].var2 << ",t" << t[tsize].num << ")"; // 弹栈 3 个元素,压入 E,endchar 设为新临时变量 popstack(state_stack, pointer_state, 3); popstackc(stack, pointer, 3); char c = 'E'; pushstack(stack, pointer, state_stack, pointer_state, c, c_out, 1); string s = get_tx(t[tsize].num); stack[(*pointer)].endchar = s; }关键点有三个:第一,取操作数的顺序不能反,E在栈中位置比T低,所以先取T再回退取E;第二,弹栈数量必须和产生式右部符号数一致,E+T是三个符号就弹三个;第三,压栈后立刻把新临时变量名写入endchar,否则上层规约取不到值。r4(T→T*F)逻辑完全对称,只是操作符换成*,取操作数时先取F再回退取T。
2.4 分析表驱动的规约调度
整个分析器的主循环是标准的 LR 驱动:查table[i][j]得到动作,s开头移进,r开头规约,acc接受,err报错。状态栈和符号栈同步操作,每次移进或规约后输出当前步骤的状态栈、符号栈、当前输入符号和已生成的四元式。原文用switch_method函数把每个动作分支拆开,好处是每个规约分支里可以独立写语义动作,缺点是代码重复度高。如果自己重写,可以把移进和规约的公共部分抽出来,只把语义动作做成回调或单独的函数表。
3. 从源码到运行:编译、输入与结果验证
3.1 编译环境与依赖
代码只依赖标准 C++ 库,iostream和cstring,没有外部依赖。用 g++ 直接编译即可:
g++ -o semantic_analyzer semantic_analyzer.cpp -std=c++11 ./semantic_analyzer如果用的是 Dev-C++ 或 Visual Studio,注意把#define size 1024放在所有使用size的声明之前,否则会报未定义。原文代码里size同时用于字符数组和结构体数组,宏定义位置不能挪。
3.2 输入格式与预处理
程序启动后提示Please input character string:,输入一个以#结尾的表达式,比如i+i*i#。代码在main里做了预处理:读入字符串后自动在末尾补#,并在符号栈底压入#,状态栈底压入0。注意输入不能带空格,因为cin >> str遇空格截断。如果需要测试带空格的表达式,得改用getline并手动过滤空格。
3.3 测试用例与预期四元式
用i+i*i#跑一遍,分析过程会依次输出移进和规约步骤。关键规约顺序是:先r10把第一个i归约为P,r8归约为F,r6归约为T,r3归约为E;然后处理+,移进第二个i,同样归约到T;接着处理*,移进第三个i,归约到F,执行r4生成(*, i, i, t1);再执行r1生成(+, i, t1, t2)。最终输出四元式序列:
(*, i, i, t1) (+, i, t1, t2)验证方法:把四元式按顺序回代,t1 = i * i,t2 = i + t1,和原表达式i+i*i的运算优先级一致。如果输出顺序反了,说明规约时取操作数的顺序搞错了。
3.4 状态栈和符号栈的对照观察
程序每步输出状态栈和符号栈的当前内容,这是排查问题的关键。比如执行r4之前,状态栈顶应该是... 5 9 5这样的模式,符号栈顶是T * F。如果发现规约时栈顶符号不对,多半是前一步移进或规约的弹压数量错了。建议在switch_method里加一行打印pointer和pointer_state的当前值,对照分析表确认状态转移是否正确。
4. 避坑与排查:语义动作挂载的五个血泪经验
4.1 规约时取操作数顺序反了
现象:i-i输出(-, i, i, t1)看不出问题,但i/i或i-i嵌套时结果变量对应关系错乱。原因:E→E-T规约时先取了栈顶T的endchar,但误把它当左操作数。解决:记住栈的生长方向,E先入栈在低位,T后入栈在高位,取左操作数要回退指针,取右操作数直接用栈顶。
4.2 弹栈数量与产生式右部长度不匹配
现象:规约后状态栈和符号栈长度不一致,后续查表越界或死循环。原因:E→E+T右部三个符号,弹栈时只弹了两个,或者E→T右部一个符号却弹了三个。解决:每个规约分支里弹栈次数严格等于产生式右部符号数,弹完立刻压入左部非终结符,压栈时状态从分析表取。
4.3 endchar 未在新压入符号上更新
现象:连续规约时上层取到的endchar是空字符串或旧值,四元式里操作数变成空白。原因:压栈后忘记执行stack[(*pointer)].endchar = s;。解决:每次pushstack之后,如果压入的是非终结符,必须把当前规约生成的临时变量名写入endchar;如果压入的是终结符i,endchar初始化为"i"。
4.4 临时变量编号重复或跳号
现象:四元式里出现两个t1,或者从t1直接跳到t3。原因:tsize在多个规约分支里被重复递增,或者某个分支忘记递增。解决:把tsize++和t[tsize].num = tsize + 1封装成一个函数new_temp(),所有规约分支统一调用,避免手写遗漏。
4.5 输入串未补 # 或符号栈底未初始化
现象:程序一启动就报Error! This character string is not this grammer's sentence.原因:main里忘记在输入串末尾补#,或者符号栈底没有压#、状态栈底没有压0。解决:读入字符串后立即执行str[length]='#'; str[length+1]='\0';,然后pointer++; stack[pointer].content='#'; pointer_state++; state_stack[pointer_state]='0';。
5. 进阶技巧:用语法树反推四元式顺序与错误恢复
5.1 从语法树验证四元式顺序
原文最后提到“运行结果是根据文法产生的下面的语法树的语句”,这句话点出了一个验证技巧:先手动画出表达式的语法树,然后后序遍历,每遇到一个内部节点就生成一条四元式。比如i+i*i的语法树根是+,左子i,右子*,右子的左右子都是i。后序遍历先处理*节点生成(*, i, i, t1),再处理+节点生成(+, i, t1, t2)。如果程序输出的四元式序列和手算后序一致,说明语义动作挂载正确。这个方法比反复跑测试用例更可靠,因为语法树是唯一的,四元式顺序也是唯一的。
5.2 错误恢复:让分析器报错后继续跑
原文代码在production == "err"时只打印错误信息就返回,整个分析中断。实际实验报告里如果要求“理解并处理语义分析中的异常和错误”,可以加一个简单的错误恢复:遇到err时弹出一个状态和符号,跳过当前输入符号,继续查表。具体做法是在switch_method的err分支里不直接return,而是执行popstack和popstackc各一次,然后(*index)++跳过当前字符。这样分析器能报告多个错误而不是一遇到错就停。注意恢复后要检查状态栈是否为空,空了就终止循环。
5.3 把四元式输出重定向到文件
实验报告需要附测试结果,手动复制控制台输出容易漏行。在main开头加一行freopen("quad_output.txt", "w", stdout);,所有cout内容会写入文件,跑完测试用例后直接打开文件整理。如果同时还想在控制台看,可以用tee思路:把输出同时写到ostringstream和文件,最后统一打印。不过最简单的方式还是重定向,跑完再cat出来看。
5.4 扩展运算符优先级:加一元负号
原文法没有一元负号,但实际表达式里-i+i这种写法很常见。扩展方法是在文法里加一条F→-F,对应规约时生成(neg, -, F, t)或(neg, F, _, t)。分析表需要新增一列或一行,状态数也会增加。如果不想改分析表,可以在词法分析阶段把-i识别为一个带符号的标识符,但这样语义动作里要处理符号位,不如直接改文法清晰。
5.5 用 Python 快速验证四元式语义
C++ 程序输出四元式后,可以用一段 Python 脚本快速验证语义是否正确:
quads = [('*', 'i', 'i', 't1'), ('+', 'i', 't1', 't2')] env = {'i': 3} # 假设 i 的值为 3 for op, a, b, res in quads: va = env.get(a, int(a) if a.isdigit() else None) vb = env.get(b, int(b) if b.isdigit() else None) if op == '*': env[res] = va * vb elif op == '+': env[res] = va + vb elif op == '-': env[res] = va - vb elif op == '/': env[res] = va / vb elif op == '^': env[res] = va ** vb print(env['t2']) # 应输出 12把i设为 3,i+i*i的结果是 12,和四元式回代一致。这个方法适合在实验报告里做交叉验证,比单纯贴控制台截图更有说服力。
从那以后我每次做语法制导翻译的实验,都先把语法树画出来,再对照四元式序列逐条核对操作数顺序和临时变量编号,确认无误后才写进报告。希望帮到你。
本文还有配套的精品资源,点击获取