首页 / 资讯中心 / 文章详情

天津理工大学编译原理实验3:语义分析与中间代码生成实战指南

天津理工大学编译原理实验3:语义分析与中间代码生成实战指南 ★ FEATURED ARTICLE
简介本资源为天津理工大学编译原理实验三的完整实验报告面向计算机与通信工程学院修读编译原理课程的学生聚焦语义分析与中间代码生成这一核心环节。报告以文法 G[E] 为对象要求从 LL1 分析法、算符优先分析法或 LR 分析法中择一构造属性文法描述并在实验二语法分析的基础上完成语法制导翻译程序设计最终输出与测试用例等价的四元式中间代码序列。压缩包内为 1 个 doc 文档约 381KB共 17 页完整记录了实验内容、目的、要求、过程记录、结果与结论并附有源程序代码包括 variable_T 与 char_stack 结构体定义、二维分析表 table 以及四元式生成逻辑便于读者对照理解语义动作的嵌入方式与错误处理思路。目前已有 376 人学习下载适合需要完成同类实验、复习语法制导翻译原理或参考四元式生成实现的学习者。1. 天津理工大学编译原理实验3语义分析与中间代码生成到底在做什么如果你正在做天津理工大学编译原理实验3大概率已经过了词法分析和语法分析两关手里有一个能跑通的语法分析器但接下来该往哪走、语义分析到底分析什么、中间代码生成又该怎么落地可能还比较模糊。这个实验的核心目标很明确在语法分析的基础上对源程序进行语义检查并生成一种中间表示形式——通常是四元式。它解决的是“语法正确但语义未必合法”的问题比如变量未声明就使用、类型不匹配、重复定义等。适合已经完成实验1和实验2、掌握了LR分析法或递归下降法的同学。说白了实验3就是让你的编译器从“能识别句子结构”进化到“能理解句子含义并翻译成中间语言”。这个阶段你会第一次真正接触到符号表管理、类型检查和四元式生成这三个硬骨头也是整个编译原理实验链条里最能体现“编译器思维”的一环。2. 语义分析的核心任务与符号表设计从属性文法到LR分析驱动的落地2.1 语义分析到底在检查什么类型、作用域与声明顺序语义分析不是玄学它要干的事情可以归结为三类第一检查变量和函数是否先声明后使用第二检查运算和赋值的类型是否匹配第三检查作用域规则是否被遵守比如内层变量是否遮蔽外层、函数参数个数是否对得上。在天津理工的实验框架里通常要求你基于已有的语法分析器在归约的时候顺带执行语义动作。常见做法是给每个非终结符附加综合属性或继承属性用属性文法来描述语义规则。比如遇到赋值语句id expr时你要检查id是否已经在符号表中声明过expr的类型是否和id的类型兼容。如果语法分析用的是LR分析法那么语义动作通常挂在产生式归约的那一步通过一个语义栈来传递类型、值、符号表入口等属性。这里最容易翻车的地方是很多同学把语义检查和语法分析完全割裂开先跑完语法分析再遍历语法树做语义结果发现符号表的作用域信息已经丢了。我一般会建议在语法分析的同时同步维护符号表归约到声明语句时插入符号归约到表达式时查询符号。2.2 符号表的数据结构选型从线性表到散列表的取舍符号表是语义分析的记忆中枢它要支持插入、查找、删除作用域退出时三种基本操作。对于天津理工实验3的规模源程序通常不会超过几百行所以用线性表加二分查找或者简单的散列表都够用。但如果你想让实验看起来更扎实我建议用散列表加作用域栈的结构。具体来说每个作用域对应一个散列表进入新作用域时压栈退出时弹栈并销毁对应的散列表。这样查找变量时从栈顶往下找天然支持遮蔽规则。下面是一个用Python描述的符号表核心结构你可以直接抄作业class SymbolTable: def __init__(self): # 作用域栈每个元素是一个字典key是变量名value是类型和附加信息 self.scopes [{}] def enter_scope(self): # 进入新作用域压入一个空字典 self.scopes.append({}) def exit_scope(self): # 退出当前作用域弹出栈顶字典 if len(self.scopes) 1: self.scopes.pop() def insert(self, name, type_info): # 在当前作用域插入符号如果已存在则报重复定义错误 current self.scopes[-1] if name in current: raise SemanticError(f重复定义: {name}) current[name] type_info def lookup(self, name): # 从栈顶往下查找找到第一个匹配的返回 for scope in reversed(self.scopes): if name in scope: return scope[name] raise SemanticError(f未声明: {name})这段代码的逻辑很直白scopes列表的末尾永远是当前作用域enter_scope和exit_scope成对出现对应语法分析中的{和}。insert只检查当前作用域是否重复lookup则从内到外逐层查找。参数说明name是标识符字符串type_info可以是一个元组(类型, 附加信息)比如(int, None)或者(array, (10, int))。注意很多同学在退出作用域时忘记弹出导致内层变量泄漏到外层这是最常见的翻车点之一。2.3 用LR分析法驱动语义动作归约时该做什么如果你的语法分析器是基于LR分析法的那么语义动作的触发点就在归约产生式的时候。LR分析器维护一个状态栈和一个语义栈状态栈记录DFA状态语义栈记录对应文法符号的属性值。当用产生式A - α归约时你从语义栈弹出|α|个元素计算A的综合属性再压回去。对于语义分析你需要在归约到声明、赋值、表达式等产生式时执行相应的检查。举个例子假设有产生式declaration - type id ;归约时你要做的是从语义栈取出type的类型信息和id的名字调用symbol_table.insert(id, type)。再比如assignment - id expression ;归约时要检查id是否已声明、类型是否和expression兼容。下面是一个简化的LR语义动作框架def reduce_action(production, semantic_stack, symbol_table): # production 是产生式对象包含左部和右部符号列表 if production.left declaration: # 右部是 type id ; 三个符号弹出三个语义值 id_name semantic_stack.pop() type_info semantic_stack.pop() semantic_stack.pop() # 弹出分号占位 symbol_table.insert(id_name, type_info) semantic_stack.append(None) # declaration 没有值压入占位 elif production.left assignment: expr_type semantic_stack.pop() id_name semantic_stack.pop() semantic_stack.pop() # 等号 semantic_stack.pop() # 分号 declared_type symbol_table.lookup(id_name) if declared_type ! expr_type: raise SemanticError(f类型不匹配: {id_name}) semantic_stack.append(None)逻辑说明semantic_stack和状态栈同步操作归约时弹出右部符号对应的语义值计算左部符号的语义值后压入。参数方面production需要你从语法分析器那边传过来通常是一个包含left和right属性的对象。注意分号、等号这类终结符也要占一个语义栈位置通常压入None即可。这里的关键是语义动作必须和归约顺序严格一致否则栈会错位查出来的类型全是乱的。3. 中间代码生成四元式设计、生成时机与回填技术3.1 四元式长什么样字段定义与常见指令集四元式是中间代码生成最常用的形式每条指令四个字段(op, arg1, arg2, result)。op是操作符比如、-、、j、jnzarg1和arg2是操作数可以是变量名、常量或者临时变量result是存放结果的地方通常是临时变量或目标变量。对于天津理工实验3你至少需要支持算术运算、赋值、条件跳转和无条件跳转。下面是一个四元式的Python表示和几个典型例子class Quadruple: def __init__(self, op, arg1, arg2, result): self.op op self.arg1 arg1 self.arg2 arg2 self.result result def __str__(self): return f({self.op}, {self.arg1}, {self.arg2}, {self.result}) # 示例a b c * d 的四元式序列 # (*, c, d, t1) # (, b, t1, t2) # (, t2, _, a)逻辑说明乘法先算生成临时变量t1加法用b和t1生成t2最后赋值给a。参数说明arg2在赋值和跳转指令中可能为空用_占位。临时变量的命名可以用t1, t2, ...递增也可以用带作用域前缀的名字避免冲突。注意四元式的顺序就是最终目标代码的执行顺序所以生成的时候必须保证语义正确。3.2 表达式和控制流的四元式生成从语法树到线性序列生成四元式最自然的方式是在语法分析归约时同步进行和语义动作合在一起。对于表达式采用后序遍历的思路先生成子表达式的四元式再生成当前操作的四元式。对于控制流比如if-else和while需要用到回填技术。回填的核心思想是先产生跳转指令但跳转目标暂时空着等目标确定后再填回去。下面是一个生成if (a b) then x 1 else x 2四元式的简化流程def gen_if_quad(cond_quad_list, then_quad_list, else_quad_list): # cond_quad_list 已经生成了条件表达式的四元式结果在临时变量 t_cond 中 # 假设 t_cond 为真时继续执行 then 分支 quads [] quads.extend(cond_quad_list) # 生成条件跳转假跳转到 else 分支目标待回填 jnz_quad Quadruple(jnz, t_cond, _, _) quads.append(jnz_quad) # then 分支 quads.extend(then_quad_list) # then 分支结束后跳过 else目标待回填 jmp_quad Quadruple(j, _, _, _) quads.append(jmp_quad) # 回填 jnz 的目标为 else 分支的起始位置 jnz_quad.result len(quads) # else 分支 quads.extend(else_quad_list) # 回填 jmp 的目标为 else 分支结束后的位置 jmp_quad.result len(quads) return quads逻辑说明jnz指令在条件为真时跳转到result指定的位置这里我们让它跳转到else分支的起始处所以条件为真时反而跳过then不对这里需要仔细。通常jnz是“非零跳转”如果t_cond为真非零应该执行then分支所以jnz的目标应该是then的起始位置。但上面的代码把jnz放在then之前目标回填为else的起始位置那就变成了条件为真时跳到else逻辑反了。正确的做法是用jz零跳转跳到else或者用jnz跳到then但把then放在跳转之后。我一般会统一用jz和j配合jz t_cond, _, else_start然后then分支然后j t_cond, _, end最后else分支。回填的时候else_start就是else分支第一条四元式的索引end就是整个if-else之后的下一条索引。参数说明四元式的result字段在跳转指令中存放目标索引用整数表示。注意回填的索引是从0开始还是从1开始要统一否则跳转全错。3.3 回填技术的实现细节链式回填与拉链当控制流嵌套时一个跳转指令的目标可能依赖于外层结构的结束位置这时候需要把多个待回填的跳转指令串成一条链等目标确定后一次性回填。常见做法是用next字段把四元式串起来或者维护一个待回填列表。下面是一个链式回填的示例class Quadruple: def __init__(self, op, arg1, arg2, result): self.op op self.arg1 arg1 self.arg2 arg2 self.result result self.next None # 用于回填链 def backpatch(quad_list, target): # quad_list 是待回填的四元式列表target 是目标索引 for quad in quad_list: quad.result target # 示例while 循环的回填 # 假设 cond_quads 生成条件body_quads 是循环体 # 循环开始位置 loop_start len(all_quads) all_quads.extend(cond_quads) # 条件为假时跳出循环目标待回填 jz_quad Quadruple(jz, t_cond, _, _) all_quads.append(jz_quad) # 循环体 all_quads.extend(body_quads) # 无条件跳回循环开始 all_quads.append(Quadruple(j, _, _, loop_start)) # 回填 jz 的目标为循环结束后的位置 jz_quad.result len(all_quads)逻辑说明backpatch函数遍历待回填列表把result统一设为目标索引。在while循环中jz指令在条件为假时跳出目标就是循环体之后的下一条指令索引。参数说明loop_start是循环条件的第一条四元式索引jz_quad.result最终被设为len(all_quads)也就是循环结束后的位置。注意如果循环体里还有嵌套的if或while它们的回填链要独立管理不能混在一起否则跳转目标会串位。4. 避坑与排查语义分析和四元式生成中最容易翻车的五个地方4.1 符号表作用域没弹栈内层变量泄漏到外层现象在if块里声明的变量出了if块还能被访问语义检查不报错。原因进入作用域时压栈了但退出时忘记弹栈或者弹栈的时机不对比如在归约if语句结束时没有触发exit_scope。解决在语法分析器中明确标记作用域的开始和结束产生式确保enter_scope和exit_scope严格配对。可以在exit_scope里加一句打印当前栈深度方便调试。4.2 四元式临时变量命名冲突导致结果被覆盖现象嵌套表达式生成的四元式里临时变量名重复后面的计算覆盖了前面的值。原因临时变量计数器是全局的但生成顺序和嵌套深度不匹配或者用了固定名字如t1没有递增。解决用一个全局计数器每次需要新临时变量时counter 1并返回t{counter}。不要手动指定临时变量名全部走统一接口。4.3 回填目标索引差一跳转跳到错误位置现象if-else或while生成的跳转指令目标差一条导致多执行或少执行一条指令。原因四元式列表的索引从0开始还是从1开始没统一或者回填时用了len(quads)但目标应该是len(quads) - 1。解决统一约定索引从0开始回填目标为下一条待生成指令的索引即当前len(quads)。在回填后打印四元式列表人工核对跳转目标。4.4 类型检查只查了声明没查运算兼容性现象int和float相加不报错但生成的中间代码没有类型转换后续解释执行时结果错误。原因语义分析只检查了变量是否声明没有检查表达式运算的类型兼容性。解决在归约算术表达式时检查左右操作数类型如果不一致则报错或插入类型转换四元式。天津理工实验通常要求报错即可但如果你想做得更完整可以生成int2float之类的转换指令。4.5 LR语义栈和状态栈不同步归约时取错属性现象语义动作里取出的类型信息对不上比如把变量名当成了类型。原因状态栈和语义栈的压弹不同步或者某些终结符没有压入语义栈占位。解决确保每个文法符号在移进时都压入语义栈终结符可以压None或具体值。归约时弹出的个数必须等于产生式右部符号个数。可以在每次压弹时打印栈内容对比状态栈和语义栈的长度。5. 进阶技巧用四元式解释器验证你的中间代码对不对生成四元式之后怎么验证它是对的最直接的办法是写一个四元式解释器逐条执行四元式看最终变量值是否符合预期。这个技巧在天津理工实验3的验收阶段特别管用因为老师通常会给你几个测试用例你跑一遍解释器就能知道中间代码有没有语义错误。下面是一个极简的四元式解释器核心逻辑def interpret(quads, variables): # quads 是四元式列表variables 是变量名到值的字典 pc 0 # 程序计数器 while pc len(quads): q quads[pc] if q.op : variables[q.result] variables[q.arg1] variables[q.arg2] elif q.op -: variables[q.result] variables[q.arg1] - variables[q.arg2] elif q.op *: variables[q.result] variables[q.arg1] * variables[q.arg2] elif q.op /: variables[q.result] variables[q.arg1] / variables[q.arg2] elif q.op : # 赋值arg1 是源result 是目标 if q.arg1 in variables: variables[q.result] variables[q.arg1] else: variables[q.result] int(q.arg1) # 常量 elif q.op j: pc q.result continue elif q.op jz: if variables[q.arg1] 0: pc q.result continue elif q.op jnz: if variables[q.arg1] ! 0: pc q.result continue pc 1 return variables逻辑说明pc是程序计数器默认每条指令执行后加1遇到跳转指令则直接修改pc并continue跳过自增。参数说明variables字典初始包含所有输入变量的值临时变量在执行过程中动态加入。注意除法这里没处理除零实际用的时候加个判断。这个解释器只有几十行但能帮你快速定位四元式生成中的逻辑错误比如跳转目标错了、临时变量没赋值等。我自己的习惯是每生成完一个测试用例的四元式先肉眼扫一遍跳转目标再跑解释器对答案。如果解释器结果和预期不符就在解释器里加打印看是哪条四元式开始偏的。这个笨办法帮我省了无数个熬夜调回填的晚上。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站