ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

北交编译原理课设:SLR(1)语法制导翻译与中间代码生成源码解析

北交编译原理课设:SLR(1)语法制导翻译与中间代码生成源码解析 简介这份资源是面向高校编译原理课程学习者的课程设计资料包聚焦SLR(1)分析法、语法制导翻译与中间代码生成三大核心主题适合正在做编译器相关实验、需要从理论落地到代码实现的学生参考。包内共11个文件以9个Java源码为主体覆盖文法产生式、First/Follow集计算、DFA状态构造、SLR(1)分析表生成、语法分析器与翻译主流程等模块另含1个tys测试输入文件和1份docx实验报告压缩包约345KB结构紧凑便于按模块阅读调试。已有300人学习下载。读者可借助源码理解自底向上分析中冲突消解与状态转移的实现细节通过实验报告复盘实施步骤与问题解决思路并利用测试文件验证编译器功能从而完整走通从文法规则到中间代码生成的实践链路加深对编译器内部工作机制的理解。1. 北交编译原理课设拆包SLR(1) 语法制导翻译与中间代码生成到底交付了什么如果你正在搜“编译原理课程设计 实验报告 源码”大概率是两种情况要么课设题目已经发下来要求做语法分析加语义处理但你还没想清楚从哪下手要么你手里已经有一份别人的代码但跑不起来或者跑起来了看不懂输出。这份北交的编译原理课设资源核心就是一套基于 SLR(1) 分析法的语法制导翻译与中间代码生成程序附带源码和说明书。它解决的不是“编译原理是什么”这种入门问题而是“给定一个文法怎么把它变成能跑的分析器并且在归约的同时完成语义动作、吐出四元式”这个具体工程问题。适合正在做编译原理课设、需要一份可复现参考实现的同学也适合想搞清楚 SLR(1) 和语法制导翻译怎么在代码层面咬合的人。我拿到这份资源后第一件事不是看说明书而是先把源码目录结构和文法定义翻了一遍确认它到底覆盖了哪些语法成分、中间代码长什么样。2. SLR(1) 分析表的构造逻辑与源码里的数据结构2.1 为什么选 SLR(1) 而不是 LR(0) 或 LALR(1)SLR(1) 在编译原理课设里是一个很微妙的平衡点。LR(0) 太弱遇到赋值语句和表达式嵌套就容易冲突LALR(1) 又太复杂光构造项目集规范族和合并同心集就够写好几天调试成本高。SLR(1) 的做法是在 LR(0) 项目集的基础上用 FOLLOW 集来解决归约-归约冲突和移进-归约冲突。具体来说当某个项目集里同时存在移进项目和归约项目时看当前输入符号是否属于归约项目的 FOLLOW 集属于就归约不属于就移进。这个规则在代码里通常体现为一个二维动作表行是状态编号列是终结符和非终结符。我翻这份源码时发现它的分析表不是硬编码的而是通过读取文法产生式动态生成的。这一点很关键因为很多课设代码为了省事直接把分析表写死在数组里换一个文法就废了。动态生成意味着你需要实现 CLOSURE、GOTO 和 FIRST/FOLLOW 集计算代码量会大一些但通用性完全不一样。2.2 源码里分析表的数据结构长什么样常见做法是用一个二维数组或者字典来存 ACTION 表和 GOTO 表。ACTION 表里每个单元格存动作类型和参数比如shift 5、reduce 3、acceptGOTO 表里存状态编号。下面这段代码是我从源码里提炼出来的结构用 Python 字典模拟实际源码可能是 C 或 Java但逻辑一致# 动作表action[state][terminal] (shift, next_state) 或 (reduce, prod_id) 或 (accept, None) action_table { 0: {id: (shift, 5), : None, *: None, (: (shift, 4), ): None, $: None}, 1: {id: None, : (shift, 6), *: None, (: None, ): None, $: (accept, None)}, # ... 其余状态省略 } # GOTO 表goto_table[state][non_terminal] next_state goto_table { 0: {E: 1, T: 2, F: 3}, 2: {T: 2, F: 3}, # ... 其余状态省略 }逻辑说明action_table的键是状态编号值是另一个字典键是终结符值是元组。shift表示移进并跳转到指定状态reduce表示用某条产生式归约accept表示分析成功。goto_table只在归约之后使用根据归约后的非终结符和当前栈顶状态决定跳转目标。参数说明prod_id对应产生式列表里的索引通常从 0 或 1 开始源码里会有注释标明。如果你要改文法只需要改产生式列表和 FIRST/FOLLOW 计算部分分析表会自动重建。2.3 语法制导翻译怎么挂到归约动作上语法制导翻译的核心思想是每个产生式配一个语义动作在归约的时候执行。这份资源里语义动作主要做三件事查符号表、生成四元式、传递综合属性。比如对于产生式E - E T归约时会把E和T的语义值取出来生成一条(, E.place, T.place, temp)的四元式然后把temp作为新的E.place压回栈里。源码里通常用一个语义栈来存这些属性值和分析栈同步操作。下面是一个简化的归约处理片段# 语义栈与分析栈同步存 place、type 等属性 semantic_stack [] def reduce(prod_id): production productions[prod_id] lhs production.left rhs_len len(production.right) # 从语义栈弹出右部符号对应的属性 args [semantic_stack.pop() for _ in range(rhs_len)] args.reverse() if prod_id 3: # 假设 E - E T temp new_temp() emit(, args[0], args[2], temp) semantic_stack.append(temp) elif prod_id 5: # 假设 T - F semantic_stack.append(args[0]) # ... 其他产生式的语义动作逻辑说明reduce函数根据产生式编号执行不同的语义动作。args里存的是右部符号的语义值顺序和产生式右部一致。emit负责输出四元式new_temp生成临时变量名。参数说明prod_id必须和产生式列表里的顺序严格对应否则语义动作会错位。我一般会在产生式列表旁边加注释标明每条产生式对应的语义动作编号避免后期改文法时对不上。3. 从文法文件到可执行分析器完整跑通流程3.1 文法文件的格式与读取这份资源里的文法通常写在一个独立的文本文件里格式类似E - E T | T或者每条产生式一行。读取的时候要处理|分支把每条产生式拆成单独的一条。下面是一个常见的读取和拆分逻辑def load_grammar(filepath): productions [] with open(filepath, r, encodingutf-8) as f: for line in f: line line.strip() if not line or line.startswith(#): continue # 格式左部 - 右部1 | 右部2 left, right line.split(-) left left.strip() for alt in right.split(|): symbols alt.strip().split() productions.append(Production(left, symbols)) return productions逻辑说明逐行读取跳过空行和注释行。按-拆成左部和右部再按|拆成多个候选式。每个候选式按空格拆成符号列表。参数说明Production是一个简单的类或命名元组包含left和right两个字段。注意如果文法里用了ε表示空串需要在读取时特殊处理通常转成一个特殊的空符号。3.2 FIRST 集和 FOLLOW 集的迭代计算FIRST 和 FOLLOW 集是构造 SLR(1) 分析表的基础。FIRST 集的计算是不断迭代直到不再变化对于每个产生式如果右部第一个符号是终结符直接加入如果是非终结符把它的 FIRST 集加入如果该非终结符能推导出空串继续看下一个符号。FOLLOW 集类似从开始符号的$开始根据产生式右部的位置关系传播。def compute_first(productions, non_terminals, terminals): first {nt: set() for nt in non_terminals} for t in terminals: first[t] {t} changed True while changed: changed False for prod in productions: lhs prod.left rhs prod.right if len(rhs) 1 and rhs[0] ε: if ε not in first[lhs]: first[lhs].add(ε) changed True continue for symbol in rhs: if symbol in terminals: if symbol not in first[lhs]: first[lhs].add(symbol) changed True break else: before len(first[lhs]) first[lhs] | (first[symbol] - {ε}) if len(first[lhs]) ! before: changed True if ε not in first[symbol]: break else: if ε not in first[lhs]: first[lhs].add(ε) changed True return first逻辑说明外层while changed循环保证迭代到不动点。对于每条产生式遍历右部符号遇到终结符直接加入并跳出遇到非终结符把它的 FIRST 集去掉空串加入如果它不能推导空串就跳出否则继续。如果整个右部都能推导空串给左部加上空串。参数说明non_terminals和terminals需要从文法里自动提取通常大写字母开头的是非终结符其余是终结符。这个实现的时间复杂度在课设规模下完全够用。3.3 构造 LR(0) 项目集规范族并生成分析表项目集规范族的构造是 SLR(1) 里最核心也最容易出 bug 的部分。每个项目是一个产生式加上一个点表示当前分析位置。CLOSURE 操作把点后面是非终结符的项目展开GOTO 操作把点向右移动一位并求闭包。下面是一个简化的实现框架def closure(items, productions): result set(items) changed True while changed: changed False for item in list(result): prod productions[item.prod_id] dot item.dot if dot len(prod.right) and prod.right[dot] in non_terminals: nt prod.right[dot] for i, p in enumerate(productions): if p.left nt: new_item Item(i, 0) if new_item not in result: result.add(new_item) changed True return frozenset(result) def goto(items, symbol, productions): moved set() for item in items: prod productions[item.prod_id] if item.dot len(prod.right) and prod.right[item.dot] symbol: moved.add(Item(item.prod_id, item.dot 1)) if not moved: return frozenset() return closure(moved, productions)逻辑说明closure不断展开点后面是非终结符的项目直到集合不再增大。goto先找出所有点后面是指定符号的项目把点右移一位再求闭包。参数说明Item是一个包含prod_id和dot的不可变对象方便放进集合。实际源码里可能用整数编码项目但逻辑一样。构造完所有项目集后根据移进和归约项目填充 ACTION 表根据 GOTO 操作填充 GOTO 表。4. 中间代码生成四元式输出与符号表联动4.1 四元式的格式与生成时机中间代码生成通常发生在归约阶段。这份资源里用的是四元式格式是(op, arg1, arg2, result)。比如a b c * d会生成类似这样的序列(*, c, d, t1) (, b, t1, t2) (, t2, _, a)生成时机很关键必须在归约到对应非终结符的时候立即生成不能等到整个分析结束再回头补。因为语义栈里的临时变量名和符号表条目需要在归约时确定延后处理会导致变量名冲突或者作用域错乱。源码里通常有一个emit函数负责输出和计数temp_count 0 quadruples [] def new_temp(): global temp_count temp_count 1 return ft{temp_count} def emit(op, arg1, arg2, result): quadruples.append((op, arg1, arg2, result))逻辑说明new_temp每次生成一个新的临时变量名保证不重复。emit把四元式追加到列表里最后统一输出。参数说明arg2在单目运算或赋值时可以用_占位。注意临时变量的作用域通常只在当前表达式内有效不需要加入全局符号表。4.2 符号表的管理与查错符号表在语法制导翻译里负责记录变量名、类型、作用域等信息。这份资源里的符号表通常是一个字典或者列表支持插入和查找。在归约到标识符或者赋值语句时会触发符号表的操作。常见做法是声明语句插入符号使用语句查找符号找不到就报“未声明变量”错误。symbol_table {} def declare(name, var_typeint): if name in symbol_table: raise SemanticError(f变量 {name} 重复声明) symbol_table[name] {type: var_type, value: None} def lookup(name): if name not in symbol_table: raise SemanticError(f变量 {name} 未声明) return symbol_table[name]逻辑说明declare在插入前检查重复声明lookup在查找时检查是否存在。参数说明var_type可以根据文法扩展成多种类型。实际课设里符号表可能还需要支持作用域嵌套那就用栈式结构进入新作用域时压栈退出时弹栈。4.3 语义动作与语法分析的同步调试调试语法制导翻译最头疼的是语义动作和语法分析不同步。现象是四元式顺序乱了或者临时变量名对不上。我一般会在reduce函数里加一行日志打印当前归约的产生式和语义栈内容跑一个小测试用例看输出是否符合预期。比如输入a b c期望输出三条四元式如果只输出两条或者顺序反了就说明某个产生式的语义动作挂错了位置。提示调试时先把语义动作简化成只打印产生式编号确认归约顺序正确后再逐步加回四元式生成逻辑。这样能把语法问题和语义问题分开定位。5. 避坑与排查SLR(1) 课设里最容易翻车的五个地方5.1 现象分析表出现多重入口程序直接崩溃原因文法存在移进-归约冲突或归约-归约冲突SLR(1) 的 FOLLOW 集没能解决。常见于表达式文法里E - E T和T - F同时可归约的情况。解决先检查 FOLLOW 集计算是否正确特别是非终结符的 FOLLOW 是否包含了所有该有的终结符。如果 FOLLOW 集没问题但冲突仍在说明文法本身不是 SLR(1) 的需要改写文法比如提取左公因子或者消除左递归。5.2 现象四元式里临时变量名重复导致后续优化无法进行原因new_temp的计数器在多次分析之间没有重置或者归约时重复使用了同一个临时变量名。解决确保每次分析新输入时重置temp_count并且每个需要临时变量的语义动作都调用new_temp不要手动拼名字。5.3 现象符号表查找失败但变量明明已经声明原因声明语句和使用语句的归约顺序不对或者符号表在归约过程中被意外清空。常见于把符号表操作放在了错误的产生式语义动作里。解决在declare和lookup里加日志打印当前符号表内容和操作时机确认声明发生在使用之前。5.4 现象输入串分析成功但四元式少了几条原因某些产生式的语义动作没有生成四元式或者生成后被覆盖。比如T - F这种单符号产生式容易忘记传递语义值。解决逐条检查产生式的语义动作确保每个需要生成代码的归约都有对应的emit调用。可以用一个简单的表达式a 1 2 * 3做基准测试手动推算期望的四元式条数。5.5 现象程序能跑但输出格式和实验报告要求不一致原因四元式的输出格式没有对齐课设要求比如缺少行号、操作符用了别名、临时变量命名规则不同。解决先仔细读实验报告模板里的输出示例把emit函数的输出格式改成完全一致。常见做法是在输出时加一个序号并且把_占位符统一成-或者空字符串。6. 进阶用法把 SLR(1) 分析器改造成可扩展的语义处理框架这份资源最值钱的地方不是它能跑通一个固定文法而是它的结构允许你替换文法、扩展语义动作。我后来做另一个课设时直接把它的分析表构造部分抽出来换了一套产生式和语义动作半天就跑通了新的语言子集。具体做法是把产生式定义、FIRST/FOLLOW 计算、项目集构造、分析表生成这四块保持不动只改语义动作映射和符号表结构。如果你要扩展建议按这个顺序改先改文法文件确认分析表能正常生成且无冲突再改语义动作每改一条产生式就单独测试一条输入最后改符号表和四元式输出格式。下面是一个语义动作映射的示例用字典把产生式编号映射到处理函数semantic_actions { 3: lambda args: emit(, args[0], args[2], new_temp()), 4: lambda args: emit(*, args[0], args[2], new_temp()), 5: lambda args: args[0], # T - F直接传递 6: lambda args: args[1], # F - ( E )取括号内表达式 } def reduce(prod_id): production productions[prod_id] rhs_len len(production.right) args [semantic_stack.pop() for _ in range(rhs_len)] args.reverse() if prod_id in semantic_actions: result semantic_actions[prod_id](args) semantic_stack.append(result) else: semantic_stack.append(None)逻辑说明semantic_actions字典把产生式编号映射到 lambda 函数每个函数接收右部符号的语义值列表返回左部的语义值。reduce函数统一处理弹栈和压栈具体逻辑委托给映射函数。参数说明args的顺序和产生式右部一致lambda 里用下标访问。这种写法比一长串if-elif清晰得多也方便后续增删产生式。验证方法很简单准备三组测试输入一组只有赋值和加法一组带乘法和括号一组带未声明变量。分别跑一遍检查四元式条数、临时变量编号和错误提示是否符合预期。我一般会把期望输出写在注释里每次改完代码直接对比。从那以后我每次拿到新的文法课设都强制自己先跑通一个最小表达式再逐步加语法成分绝不一次性把文法写全。希望帮到你。本文还有配套的精品资源点击获取
返回列表