ARTICLE DETAIL

资讯详情

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

北交编译原理大作业:SLR(1)语法制导翻译与中间代码生成实战

北交编译原理大作业:SLR(1)语法制导翻译与中间代码生成实战 简介这份资源是面向高校编译原理课程学习者的课程设计资料包聚焦SLR(1)分析法、语法制导翻译与中间代码生成三大核心主题适合正在做编译器相关实验或需要理解自底向上语法分析流程的学生参考。包内共11个文件以9个Java源码文件为主体涵盖SLR(1)分析表构建、First/Follow集计算、DFA状态管理、产生式与翻译文法定义等模块另含1个tys测试输入文件和1份docx实验报告压缩包约345KB体量轻便便于本地调试。已有300人学习下载。读者可借助源码直观理解分析表构造与语法制导翻译的实现细节通过实验报告梳理实施步骤、冲突处理与排错思路并利用测试文件验证编译器功能从而完成从文法规则到中间代码生成的完整实践链路。1. 从一道北交编译原理大作业说起SLR(1) 语法制导翻译到底在做什么如果你正在搜「北交 编译原理 基于SLR(1)分析法的语法制导翻译及中间代码生成程序设计原理与实现」大概率是两种情况要么你手里已经拿到了那个 zip里面有源码和说明书但打开一看不知道从哪读起要么你正准备自己动手写想搞清楚 SLR(1) 和语法制导翻译、中间代码生成这三件事到底怎么串成一条线。先把范围划清楚。这个题目本质上是编译原理课程里最经典的一条流水线词法分析拿到 token 流之后用 SLR(1) 分析法做语法分析在归约的每一步顺带执行语义动作也就是语法制导翻译最终把源程序翻译成四元式形式的中间代码。它不是一个玩具而是把「文法 → 分析表 → 分析栈 → 语义动作 → 四元式」这条链路完整走一遍。适合正在做编译原理实验的本科生也适合想复习自底向上分析全流程的人。下面我按自己带学生做这个题目的顺序把原理、实现、参数和坑一条条讲清楚。2. SLR(1) 分析表的构造从文法到 ACTION/GOTO 两张表2.1 为什么是 SLR(1) 而不是 LR(0) 或 LALR(1)LR(0) 的问题在于它不看展望符只要项目集里出现「移进-归约」冲突就没法决策。SLR(1) 的改进很朴素在归约的时候只有当当前输入符号落在 FOLLOW(A) 里才归约否则移进。这一条约束就能消掉大量冲突实现代价又比 LALR(1) 小得多——不需要合并同心项目集也不需要重新计算展望符传播。对于课程实验里常见的表达式文法、赋值语句、if-else、while 这些结构SLR(1) 基本够用。我一般会先确认一件事你的文法有没有「归约-归约」冲突。如果有SLR(1) 救不了你得改文法或者上 LALR(1)。这是选型的第一道门槛。2.2 构造 LR(0) 项目集规范族第一步是增广文法。假设原始开始符号是 S加一条 S → S保证接受项目唯一。然后从 I0 closure({S → ·S}) 出发对每个项目集 I 和每个文法符号 X 计算 GOTO(I, X)直到不再产生新项目集。closure 的规则如果 A → α·Bβ 在项目集里且 B → γ 是产生式就把 B → ·γ 加进去。GOTO 的规则把 I 中所有 A → α·Xβ 的点右移一位得到 A → αX·β再求 closure。# 项目用 (产生式左部, 右部元组, 点的位置) 表示 def closure(items, productions): result set(items) changed True while changed: changed False for (lhs, rhs, dot) in list(result): if dot len(rhs) and rhs[dot] in productions: for prod in productions[rhs[dot]]: new_item (rhs[dot], prod, 0) if new_item not in result: result.add(new_item) changed True return frozenset(result) def goto(items, symbol, productions): moved set() for (lhs, rhs, dot) in items: if dot len(rhs) and rhs[dot] symbol: moved.add((lhs, rhs, dot 1)) return closure(moved, productions) if moved else frozenset()这段代码里productions是一个字典键是非终结符值是右部元组的列表。closure用 while 循环反复扫描直到不再新增这是最直观的写法项目集规模小的时候完全够用。goto先做点右移再求闭包。注意frozenset是为了后面能当字典的键用。2.3 填 ACTION 表和 GOTO 表有了项目集规范族编号 I0、I1……然后逐个项目集填表。规则三条如果 A → α·aβ 在 I 中a 是终结符且 GOTO(I, a) J则 ACTION[I, a] shift J。如果 A → α· 在 I 中对每个 a ∈ FOLLOW(A)ACTION[I, a] reduce A → α。如果 S → S· 在 I 中ACTION[I, $] accept。GOTO 表只对非终结符填GOTO(I, A) J。def build_table(states, transitions, productions, follow, start_prime): action, goto_table {}, {} for i, items in enumerate(states): for (lhs, rhs, dot) in items: if dot len(rhs): sym rhs[dot] if sym in transitions.get(i, {}): j transitions[i][sym] if sym.isupper() or sym $: # 非终结符走 GOTO goto_table[(i, sym)] j else: action[(i, sym)] (shift, j) else: if lhs start_prime: action[(i, $)] (accept,) else: for a in follow[lhs]: action[(i, a)] (reduce, lhs, rhs) return action, goto_tablefollow集合需要提前算好这是 SLR(1) 和 LR(0) 唯一的区别所在。填表时如果同一个格子被写两次且值不同就是冲突必须打印出来定位。我习惯在填表函数里加一个冲突检测一旦发现就抛出异常并带上项目集编号省得后面调试时抓瞎。3. 语法制导翻译把语义动作挂到归约上3.1 综合属性与继承属性的取舍语法制导翻译分 S 属性和 L 属性。S 属性只用综合属性自底向上分析时天然适配——归约的时候子节点的值已经算好了直接往上传。L 属性允许继承属性但自底向上分析里继承属性的求值时机很别扭需要额外的手段。对于这个题目我强烈建议全部用综合属性。表达式求值、变量声明、类型检查、四元式生成这些都能用综合属性表达。语义栈和分析栈同步压弹归约时从栈顶弹出 n 个符号对应的语义值算出新值再压回去。这样代码结构最干净。3.2 语义栈与分析栈的同步分析栈里存的是状态语义栈里存的是属性值。移进的时候终结符的语义值比如标识符名字、常量值跟着压入语义栈归约的时候按产生式右部长度弹出对应数量的语义值执行语义动作把结果压回语义栈。def parse(tokens, action, goto_table, semantic_rules): state_stack [0] sem_stack [] pos 0 while True: state state_stack[-1] lookahead tokens[pos] if pos len(tokens) else ($, None) act action.get((state, lookahead[0])) if act is None: raise SyntaxError(f状态 {state} 遇到 {lookahead[0]} 无动作) if act[0] shift: state_stack.append(act[1]) sem_stack.append(lookahead[1]) # 终结符语义值入栈 pos 1 elif act[0] reduce: lhs, rhs act[1], act[2] n len(rhs) args sem_stack[-n:] if n 0 else [] if n 0: del sem_stack[-n:] del state_stack[-n:] new_state goto_table[(state_stack[-1], lhs)] state_stack.append(new_state) value semantic_rules[lhs](args) # 执行语义动作 sem_stack.append(value) elif act[0] accept: return sem_stack[-1]semantic_rules是一个字典键是产生式左部值是一个函数接收右部符号的语义值列表返回左部的语义值。归约时先弹栈再压栈的顺序不能反否则状态对不上。args在 n 为 0 时是空列表对应空产生式。3.3 四元式的数据结构与生成时机四元式就是 (op, arg1, arg2, result) 四元组。生成时机在归约的语义动作里比如处理 E → E T 的时候先拿到 E 和 T 的语义值通常是存放结果的临时变量名生成一条 (, e_val, t_val, new_temp)然后把 new_temp 作为 E 的语义值往上传。temp_count 0 def new_temp(): global temp_count temp_count 1 return ft{temp_count} quad_list [] def rule_E_plus_T(args): left, _, right args t new_temp() quad_list.append((, left, right, t)) return t def rule_E_id(args): return args[0] # 标识符本身作为值 semantic_rules { E: lambda args: rule_E_plus_T(args) if len(args) 3 else rule_E_id(args), # 其余产生式类似 }临时变量命名用 t1、t2 递增就行课程实验不要求优化的话没必要搞寄存器分配。quad_list最后按顺序输出就是中间代码。注意语义动作里不要直接打印先存列表最后统一输出方便调试和后续可能的优化。4. 中间代码生成的落地细节从语义值到四元式序列4.1 赋值语句和表达式的翻译模式赋值语句 S → id E 的语义动作拿到 id 的名字和 E 的值生成 (, E_val, _, id_name)。注意四元式里赋值通常写成 op 为 arg1 是右部值result 是左部变量。表达式要处理优先级。文法里 E → E T | TT → T * F | FF → (E) | id | num这样优先级天然由文法层次保证。语义动作挂在每个归约上加法和乘法分别生成对应的四元式。括号不生成四元式只是把内层 E 的值直接传上去。4.2 控制语句的四元式与回填if 和 while 需要跳转指令四元式里用 (j, _, _, label)、(jf, cond, _, label) 这种形式。问题是跳转目标在生成跳转指令的时候可能还不知道需要回填。常见做法是维护一个回填链表。比如 if E then S翻译 E 的时候生成 (jf, E_val, _, 0)这个 0 是占位符把这条四元式的下标记下来等 S 翻译完知道目标位置了再回填。def backpatch(quad_indices, target): for idx in quad_indices: op, a1, a2, _ quad_list[idx] quad_list[idx] (op, a1, a2, target) def rule_if(args): # args [E_val, S_quads_marker] jf_idx len(quad_list) quad_list.append((jf, args[0], _, 0)) # S 的四元式已经生成 backpatch([jf_idx], len(quad_list)) return Nonebackpatch接收一个下标列表和目标标签把占位符替换掉。while 语句需要两条回填链一条是循环体结束后跳回条件判断一条是条件为假时跳出循环。这块是中间代码生成里最容易出错的地方建议画个图把每条跳转的源和目标标清楚再写代码。4.3 符号表与类型检查的插入点符号表在语义动作里维护。声明语句 S → type id 的时候把 (id, type) 插入符号表。使用标识符的时候查表如果没找到就报「未声明」。类型检查在表达式归约时做比如 E → E T检查两边类型是否兼容不兼容就报错。symbol_table {} def rule_decl(args): type_name, id_name args if id_name in symbol_table: raise SemanticError(f重复声明: {id_name}) symbol_table[id_name] type_name return None def rule_E_plus_T_typed(args): left, _, right args lt, rt symbol_table.get(left, int), symbol_table.get(right, int) if lt ! rt: raise SemanticError(f类型不匹配: {lt} {rt}) t new_temp() quad_list.append((, left, right, t)) return t符号表用字典就够了课程实验不涉及作用域嵌套的话不需要栈式符号表。类型检查放在语义动作里比单独一遍扫描更自然因为归约的时候类型信息刚好都在手边。5. 避坑与排查SLR(1) 实验里最容易翻车的五个地方5.1 冲突没检测填表时静默覆盖现象分析表填完了跑测试用例结果莫名其妙明明文法看起来没问题。原因ACTION 表同一个格子被 shift 和 reduce 写了两次后写的覆盖了先写的冲突被吞掉了。解决填表函数里每次写入前检查 key 是否已存在存在且值不同就打印冲突详情包括项目集编号、符号、两个动作。我一般直接抛异常强制自己面对冲突。5.2 FOLLOW 集算错导致归约该发生时不发生现象遇到某个输入符号分析表里既没有 shift 也没有 reduce报「无动作」。原因FOLLOW 集漏了某个符号导致本该归约的格子是空的。解决FOLLOW 集的计算要反复迭代到不动点特别是处理 A → αB 和 A → αBβ 两种情况时β 能推出空串的话要把 FOLLOW(A) 并进 FOLLOW(B)。建议手算一个小文法的 FOLLOW 集跟程序输出对一遍。5.3 语义栈弹栈数量与产生式右部长度不一致现象归约后语义值错位四元式里参数张冠李戴。原因弹栈时用了错误的长度比如把 ε 产生式当成长度 1 处理。解决弹栈长度严格等于产生式右部的符号个数空产生式长度为 0不弹任何东西。在归约分支里打印len(rhs)和实际弹出的数量做核对。5.4 回填时标签指向错误位置现象if 语句的跳转目标差一条或差几条指令。原因回填的时机不对比如在生成跳转指令之前就记录了目标位置或者目标位置算的是四元式下标而不是实际地址。解决统一用四元式列表的下标作为标签回填时目标就是len(quad_list)表示下一条要生成的指令的位置。在回填函数里打印替换前后的四元式肉眼确认。5.5 词法分析器与语法分析器的接口对不上现象语法分析器收到的 token 类型跟分析表里的终结符名字不一致比如词法输出ID但表里写的是id。原因两边命名约定没统一。解决在项目开头就定好终结符命名规范词法分析器的 token 类型直接复用文法里的终结符名字。加一层断言token 类型不在终结符集合里就报错。6. 验证与进阶怎么确认你的翻译结果是对的跑通一个用例不代表程序正确。我一般会准备三组测试第一组是纯表达式验证四元式的运算顺序和临时变量编号第二组带赋值和声明验证符号表和类型检查第三组带 if-while 嵌套验证回填和跳转。每组手工推导期望的四元式序列跟程序输出逐条对比。一个具体技巧给四元式生成加一个可选的注释模式每条四元式后面用注释标出它是由哪条产生式归约时生成的。这样一旦发现某条四元式不对立刻能定位到对应的语义动作。def emit(op, a1, a2, result, prodNone): idx len(quad_list) quad_list.append((op, a1, a2, result)) if DEBUG: print(f{idx}: ({op}, {a1}, {a2}, {result}) - {prod}) return idxDEBUG开关控制是否打印产生式来源正式输出的时候关掉。这个习惯帮我省了大量调试时间尤其是回填出错的时候一眼就能看出是哪条归约产生的跳转指令。最后说个血泪经验不要等全部写完再测。每实现一个语义动作就跑一次对应的最小用例确认四元式对了再往下写。我见过太多人一口气写完几百行结果第一个用例就崩回头排查发现是 FOLLOW 集算错导致分析表从根上就是歪的。增量验证步步为营这个题目的坑基本都能绕过去。希望帮到你。本文还有配套的精品资源点击获取
返回列表