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) 语法制导翻译 中间代码生成」大概率是两种情况之一要么课设要求你从零实现一个能跑通文法分析、语义动作和四元式输出的完整流程要么你拿到了一个压缩包但不知道里面每个模块怎么串起来。这个标题对应的不是单一算法而是一条完整的编译前端流水线词法分析器把源程序切成 tokenSLR(1) 语法分析器边归约边触发语义动作语法制导翻译在归约时同步计算属性最终生成四元式形式的中间代码。它解决的核心问题是如何让语法分析和语义计算不脱节在自底向上的分析过程中把翻译动作嵌进去。适合已经学过 LR 分析表构造、但卡在「怎么把语义规则变成可执行代码」这一步的同学。我见过太多人语法分析表能画出来一到语义动作就不知道怎么挂这篇就把这条链路拆开讲清楚。2. SLR(1) 分析表的构造从文法到可执行动作表2.1 为什么选 SLR(1) 而不是 LR(0) 或 LALR(1)SLR(1) 的定位很明确它比 LR(0) 多了一个 FOLLOW 集来消解冲突又比 LALR(1) 少了合并同心项的复杂度。对于课设级别的文法——通常是表达式求值、赋值语句、if-else、while 循环这几类——SLR(1) 的冲突消解能力刚好够用实现难度又不会失控。我一般会先判断文法是否满足 SLR(1) 条件对每个项目集如果存在移进-归约冲突就看移进符号是否在归约项目的 FOLLOW 集中如果存在归约-归约冲突就看两个归约项目的 FOLLOW 集是否相交。不相交就能用 SLR(1)。这个判断过程本身就可以写成代码自动完成不需要手工验证。选 SLR(1) 的另一个实际理由是它的分析表结构清晰ACTION 表和 GOTO 表分开存储语义动作可以直接挂在归约项上。LALR(1) 虽然更强但项目集合并后归约项的来源变复杂对课设来说调试成本反而更高。2.2 构造 LR(0) 项目集规范族的代码实现构造项目集族是整个流程的第一步也是最容易出 bug 的地方。核心操作是闭包closure和转移goto。下面是我常用的 Python 实现骨架# 文法产生式用元组表示: (左部, [右部符号列表]) # 增广文法: S - S grammar [ (S, [S]), (S, [id, , E]), (E, [E, , T]), (E, [T]), (T, [T, *, F]), (T, [F]), (F, [(, E, )]), (F, [id]), ] def closure(items, grammar): items: 项目集合每个项目为 (产生式索引, 点的位置) result set(items) changed True while changed: changed False for prod_idx, dot in list(result): lhs, rhs grammar[prod_idx] if dot len(rhs): next_sym rhs[dot] # 如果点后面是非终结符加入该非终结符的所有产生式 for i, (l, r) in enumerate(grammar): if l next_sym: new_item (i, 0) if new_item not in result: result.add(new_item) changed True return frozenset(result) def goto(items, symbol, grammar): 计算 GOTO(I, X) moved set() for prod_idx, dot in items: lhs, rhs grammar[prod_idx] if dot len(rhs) and rhs[dot] symbol: moved.add((prod_idx, dot 1)) if not moved: return None return closure(moved, grammar)closure函数的逻辑是只要点后面是非终结符就把该非终结符的所有产生式以点在最左的形式加入集合反复迭代直到不再变化。goto函数先把点越过指定符号再做闭包。这两个函数是构造项目集族的基础参数grammar是全局产生式列表items是当前项目集。实际跑的时候要注意产生式索引必须和文法定义严格对应增广产生式放在索引 0 的位置。我踩过的坑是忘了加增广文法导致初始项目集不完整后面 GOTO 表全是错的。2.3 FIRST 集和 FOLLOW 集的自动计算FIRST 和 FOLLOW 集是 SLR(1) 消解冲突的依据手工算容易漏建议直接写函数自动求def compute_first(grammar, terminals): first {t: {t} for t in terminals} for lhs, _ in grammar: first.setdefault(lhs, set()) changed True while changed: changed False for lhs, rhs in grammar: for sym in rhs: before len(first[lhs]) first[lhs] | (first[sym] - {ε}) if ε not in first[sym]: break if sym rhs[-1]: first[lhs].add(ε) if len(first[lhs]) before: changed True return first def compute_follow(grammar, first, start_symbol): follow {lhs: set() for lhs, _ in grammar} follow[start_symbol].add($) changed True while changed: changed False for lhs, rhs in grammar: for i, sym in enumerate(rhs): if sym not in follow: continue rest rhs[i1:] if rest: first_rest set() for s in rest: first_rest | (first[s] - {ε}) if ε not in first[s]: break else: first_rest.add(ε) before len(follow[sym]) follow[sym] | (first_rest - {ε}) if ε in first_rest: follow[sym] | follow[lhs] if len(follow[sym]) before: changed True else: before len(follow[sym]) follow[sym] | follow[lhs] if len(follow[sym]) before: changed True return followcompute_first的核心是反复扫描产生式把右部第一个符号的 FIRST 集并入左部遇到可空符号继续看下一个。compute_follow则处理三种情况右部后面有符号时用 FIRST 集后面为空或可空时用左部的 FOLLOW 集。参数terminals是终结符集合start_symbol是文法开始符号。这两个函数跑完后建议打印出来和手工计算的结果对一遍。我一般会拿一个简单文法验证确认无误再上复杂文法。3. 语法制导翻译把语义动作挂到归约步骤上3.1 综合属性与继承属性的落地区别语法制导翻译的核心概念是属性文法。综合属性从子节点往父节点传继承属性从父节点往子节点传。在自底向上的 SLR(1) 分析中综合属性天然适合——因为归约时子节点的值已经算好了直接弹栈计算即可。继承属性则麻烦得多需要在移进时就把值压入栈或者用全局变量辅助。课设里常见的翻译任务——表达式求值、变量声明、类型检查——大部分只需要综合属性。比如E - E T的语义动作是E.val E1.val T.val归约时从栈里弹出三个符号取两个表达式的值相加再把结果压回去。这个模式非常统一。如果遇到需要继承属性的场景比如变量声明的作用域传递我一般会用符号表配合全局栈来模拟而不是严格按属性文法实现。课设不要求形式化证明能跑通就行。3.2 语义动作与分析栈的同步机制SLR(1) 分析器用两个栈状态栈和符号栈。语法制导翻译需要第三个栈——属性栈或者把属性直接存在符号栈的每个元素里。我倾向于后者用一个栈同时存符号和属性结构更紧凑。class SemanticStack: def __init__(self): self.stack [] # 每个元素: {symbol: str, value: any, type: str} def push(self, symbol, valueNone, type_None): self.stack.append({symbol: symbol, value: value, type: type_}) def pop(self, n1): result [] for _ in range(n): result.append(self.stack.pop()) return list(reversed(result)) def peek(self, offset0): return self.stack[-(offset 1)]归约时根据产生式右部长度弹出对应数量的元素取出它们的value做计算然后把左部符号和计算结果压回去。参数n是弹出数量value存属性值type_存类型信息用于类型检查。关键点是弹栈顺序和产生式右部顺序要一致。我见过有人弹出后直接按栈顺序取结果表达式左右操作数反了3 - 5算成5 - 3。正确做法是弹出后反转或者从后往前取。3.3 用四元式生成中间代码的翻译方案中间代码生成是语法制导翻译的最终产出。四元式格式是(op, arg1, arg2, result)适合课设因为结构简单、容易验证。以表达式a b c * d为例翻译过程如下# 四元式生成器 class QuadGenerator: def __init__(self): self.quads [] self.temp_count 0 def new_temp(self): self.temp_count 1 return ft{self.temp_count} def emit(self, op, arg1, arg2, result): self.quads.append((op, arg1, arg2, result)) return result def gen_binary(self, op, left, right): temp self.new_temp() self.emit(op, left, right, temp) return temp def gen_assign(self, target, value): self.emit(, value, -, target) return target对应的语义动作挂在归约项上F - idF.place id.lexemeT - FT.place F.placeT - T * FT.place gen_binary(*, T1.place, F.place)E - TE.place T.placeE - E TE.place gen_binary(, E1.place, T.place)S - id Egen_assign(id.lexeme, E.place)new_temp生成临时变量名emit把四元式追加到列表。参数op是操作符arg1和arg2是操作数result是存放结果的位置。对于赋值语句arg2用-占位。跑完a b c * d后四元式序列应该是(*, c, d, t1) (, b, t1, t2) (, t2, -, a)这个顺序不能乱先算乘法因为优先级高再算加法最后赋值。如果四元式顺序不对说明语义动作挂错了归约项。4. 避坑与排查SLR(1) 课设里最容易翻车的五个地方4.1 冲突消解后分析表仍然报错现象按照 SLR(1) 规则消解了移进-归约冲突但分析表里同一个格子还是填了两个动作。原因通常是 FOLLOW 集算错了。比如E - E T和E - T两个归约项如果T的 FOLLOW 集包含了而又是移进符号就会冲突。但 SLR(1) 要求的是归约项的 FOLLOW 集不是任意符号的。解决打印每个归约项的 FOLLOW 集和手工计算的对一遍。重点检查产生式右部最后一个符号的 FOLLOW 集是否正确并入了左部的 FOLLOW 集。4.2 语义动作执行时栈里取不到值现象归约时弹出元素发现value是None或者数量不对。原因移进时忘了压属性值或者归约时弹出数量算错。比如产生式E - E T右部长度是 3但只弹了 2 个。解决在push和pop里加日志每次操作打印栈的深度和内容。确认移进时压入了 token 的属性值归约时弹出数量等于产生式右部长度。4.3 四元式临时变量重复或顺序错乱现象生成的中间代码里两个不同的子表达式用了同一个临时变量名或者四元式顺序和预期不符。原因temp_count没有正确递增或者语义动作挂在了错误的归约项上。比如把T - T * F的动作挂到了T - F上。解决检查每个归约项对应的语义动作是否和产生式匹配。临时变量用全局计数器每次new_temp必须递增。可以在emit里加断言确保result不为空。4.4 词法分析器返回的 token 类型和语法分析器不匹配现象语法分析器报「意外的 token」但源程序看起来没问题。原因词法分析器把id返回成了identifier或者把数字统一返回成num但语法里写的是digit。解决在词法分析器和语法分析器之间加一层映射或者统一 token 命名规范。我一般会在词法分析器输出时打印 token 序列和语法分析器的终结符集合对一遍。4.5 分析表太大导致内存或性能问题现象文法稍微复杂一点项目集数量爆炸程序跑几分钟出不来结果。原因项目集族构造时没有去重或者闭包计算重复遍历。解决项目集用frozenset存储天然可哈希去重。闭包计算用while changed模式每次只处理新增项目。如果还是慢考虑用 LALR(1) 合并同心项但课设级别一般不需要。5. 从能跑到好用验证中间代码正确性的三个技巧5.1 用解释器反验证四元式序列生成四元式后最直接的验证方式是写一个简单的四元式解释器按顺序执行看结果和预期是否一致。这个解释器不需要复杂用一个字典模拟内存遇到(, value, -, target)就赋值遇到算术操作就计算并存入临时变量。def interpret_quads(quads, initial_envNone): env dict(initial_env or {}) for op, arg1, arg2, result in quads: if op : env[result] env.get(arg1, arg1) elif op : env[result] env.get(arg1, arg1) env.get(arg2, arg2) elif op *: env[result] env.get(arg1, arg1) * env.get(arg2, arg2) # 其他操作符按需扩展 return env这个函数接收四元式列表和初始变量值返回执行后的环境。参数initial_env用于给变量赋初值。跑完后检查目标变量的值是否和手工计算一致。我一般会准备三组测试纯算术表达式、带括号的嵌套表达式、多语句赋值。5.2 对比不同输入下的四元式结构同一个表达式用不同方式写四元式结构应该等价。比如a b c * d和a b (c * d)应该生成相同的四元式序列。如果不同说明括号处理有问题。另一个测试是交换律验证a b c和a c b的四元式操作数顺序不同但结果相同。这能验证语义动作是否正确取了左右操作数。5.3 边界输入的压力测试课设验收时老师往往会给一些边界用例。我一般会准备这几类测试类型输入示例检查点单变量赋值a b四元式只有一条赋值深层嵌套a ((b))括号不生成四元式直接透传多运算符混合a b c * d - e / f优先级和结合性正确连续赋值a b c右结合处理正确空输入空字符串不崩溃返回空四元式列表这些用例跑通后基本能覆盖课设验收的绝大多数场景。如果时间充裕还可以加一个随机表达式生成器自动对比解释器结果和 Python 原生计算结果做批量验证。5.4 我踩过的最大的坑说一个血泪经验我一开始把语义动作直接写在语法分析器的归约分支里用if prod_idx 3: ...这种硬编码。结果文法一改所有索引全乱调试了一整晚。后来改成用字典把产生式索引映射到 lambda 函数文法变了只需要改映射表分析器代码不动。这个习惯我一直保持到现在——语义动作和语法分析逻辑必须解耦否则改一处崩一片。希望帮到你。本文还有配套的精品资源点击获取
返回列表