ARTICLE DETAIL

资讯详情

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

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

天津理工大学编译原理实验3:语义分析与四元式中间代码生成实战 简介本资源为天津理工大学编译原理实验三的完整实验报告面向计算机与通信工程学院修读编译原理课程的学生聚焦语义分析与中间代码生成环节。报告以文法G[E]为对象要求从LL1分析法、算符优先分析法或LR分析法中择一构造属性文法描述并在实验二语法分析的基础上完成语法制导翻译程序设计最终输出与测试用例等价的四元式中间代码序列。压缩包内为1个doc文档约381KB共17页涵盖实验内容、目的、要求、过程记录、结果与结论等完整章节并附有源程序片段如variable_T与char_stack结构体定义、二维分析表table及四元式生成逻辑便于读者对照理解语义动作的嵌入方式与错误处理思路。目前已有376人学习下载适合需要完成同类实验、复习语法制导翻译流程或参考报告撰写结构的学生使用。1. 天津理工大学编译原理实验3语义分析与中间代码生成到底在考什么如果你正在做天津理工大学编译原理实验3大概率已经过了词法分析和语法分析那两关手里有一个能跑通的语法分析器但接下来卡住了语义分析到底要分析什么中间代码生成又该生成成什么样子这个实验的核心不是让你写一个完整的编译器后端而是让你在已有的语法分析基础上补上两件事——一是对语法树做语义检查比如变量是否声明、类型是否匹配、除数是否为零二是把语法树翻译成四元式形式的中间代码。四元式是绝大多数高校编译原理课程采用的中间表示格式就是op, arg1, arg2, result简单直接方便后续做优化和目标代码生成。LR分析法在这里的角色是你的语法分析阶段大概率用的是LR分析法或者LL语义分析和中间代码生成是建立在语法分析输出之上的所以理解LR分析过程中什么时候做归约、归约时触发什么语义动作是打通这个实验的关键。适合谁看正在做这个实验但不知道从哪下手的人以及想把语义分析和中间代码生成真正写出来而不是抄一份报告的人。2. 语义分析到底分析什么从语法树到符号表2.1 语义分析的三类核心任务语义分析不是玄学它要做的事情可以归结为三类。第一类是符号表管理每个变量、函数、类型都要在符号表里有记录包括名字、类型、作用域、内存偏移等信息。第二类是类型检查赋值语句左右类型是否兼容、算术运算的操作数是否都是数值类型、函数调用参数个数和类型是否匹配。第三类是语义合法性检查变量是否重复声明、是否使用了未声明的变量、是否有除零风险、break是否出现在循环外等。在天津理工这个实验里通常不会要求你做完整的类型系统但基本的符号表管理和类型检查是必须的。我一般会建议先定义一个符号表结构用哈希表或者简单的线性表都行关键是能支持插入、查找、作用域嵌套这三个操作。class SymbolTable: def __init__(self): self.scopes [{}] # 栈式作用域栈顶是当前作用域 def enter_scope(self): self.scopes.append({}) def exit_scope(self): self.scopes.pop() def declare(self, name, type_info): if name in self.scopes[-1]: raise SemanticError(f变量 {name} 重复声明) self.scopes[-1][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是一个栈每进入一个块比如 if、while、函数体就压入一个新字典退出时弹出。declare只在当前作用域检查重复lookup从内到外逐层查找。参数方面type_info可以是一个字符串如 int、float或者一个字典取决于你的实验要求。注意exit_scope时不需要手动清理变量因为整个字典被弹出了。2.2 在LR分析中嵌入语义动作的时机LR分析法做语法分析时语义动作通常绑定在归约产生式上。也就是说当解析器用某条产生式进行归约时同时执行对应的语义动作。比如产生式Assign - id Expr在归约时你应该检查id是否已声明、Expr的类型是否与id兼容然后生成赋值四元式。具体实现上如果你用的是Yacc/Bison这类工具语义动作直接写在产生式后面的大括号里。如果是手写的LR分析器你需要在归约函数里根据产生式编号调用对应的语义处理函数。常见做法是维护一个语义栈与语法分析栈同步操作。def reduce(self, production_id): if production_id 1: # Assign - id Expr expr_type, expr_place self.semantic_stack.pop() id_name self.semantic_stack.pop() id_type self.symbol_table.lookup(id_name) if not self.type_compatible(id_type, expr_type): raise SemanticError(f类型不匹配: {id_name}) self.emit(, expr_place, -, id_name) self.semantic_stack.append((id_type, id_name))这里semantic_stack和语法分析栈是平行的归约时从栈顶弹出右部符号对应的语义值计算后把左部符号的语义值压回去。emit函数负责生成四元式并存入四元式表。参数说明production_id是产生式编号需要和你的语法定义对应type_compatible需要你根据实验要求实现通常 int 可以赋给 float反过来不行。2.3 符号表与作用域的边界处理一个容易翻车的地方是作用域边界。比如在 if 语句的 then 分支里声明了一个变量出了这个分支之后这个变量应该不可见。如果你的符号表只有一层就会出现变量泄漏。用栈式作用域可以解决但要注意进入块时要enter_scope退出时要exit_scope而且这两个操作必须和语法结构严格对应。另一个坑是函数参数的作用域。函数参数通常属于函数体的最外层作用域而不是调用者的作用域。所以在处理函数定义时应该先enter_scope然后把参数逐个declare进去再处理函数体。注意符号表的查找效率在实验规模下不是瓶颈用列表遍历都行但作用域嵌套的正确性直接决定语义分析是否通过。3. 四元式生成从表达式到控制流的翻译套路3.1 四元式的基本格式与生成时机四元式就是四个字段操作符、第一个操作数、第二个操作数、结果。比如a b c翻译成(, b, c, t1)和(, t1, -, a)。临时变量t1由编译器自动生成通常用t1, t2, t3...编号。生成时机是在语义动作执行时。对于表达式通常采用自底向上的方式先处理子表达式把结果放在临时变量里再用临时变量作为父表达式的操作数。这就是为什么语义栈里存的不是变量名而是位置——位置可以是变量名也可以是临时变量名。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_binop(self, op, left_place, right_place): temp self.new_temp() self.emit(op, left_place, right_place, temp) return tempnew_temp每次生成一个新的临时变量名emit把四元式追加到列表中。gen_binop是二元运算的通用处理生成一个临时变量发射四元式返回临时变量名供上层使用。参数left_place和right_place是语义栈里弹出的操作数位置。3.2 控制流语句的四元式翻译控制流是四元式生成里最容易出错的部分。if-else、while、for 这些语句需要回填技术。原因是当你翻译if (cond) then_stmt时翻译完cond之后你还不知道 then 块的第一条四元式在哪里所以条件跳转的目标地址暂时空着等 then 块翻译完再回填。常见做法是维护一个回填表backpatch list记录所有等待回填的四元式编号。def gen_if(self, cond_place, then_start): # cond_place 是条件表达式的结果位置 # 生成条件为假时跳转到 else 或结束 jump_quad self.emit(jz, cond_place, -, None) # 目标待回填 return len(self.quads) - 1 # 返回四元式编号 def backpatch(self, quad_index, target): op, arg1, arg2, _ self.quads[quad_index] self.quads[quad_index] (op, arg1, arg2, target)gen_if发射一条jzjump if zero四元式目标地址先填None返回这条四元式的索引。backpatch在后续知道目标位置后把None替换成实际的目标四元式编号。参数quad_index是四元式在列表中的下标target是跳转目标的下标。对于 while 循环翻译顺序是记录循环开始位置 → 翻译条件 → 发射条件为假时跳出循环的四元式 → 翻译循环体 → 发射无条件跳回循环开始的四元式 → 回填跳出地址。3.3 布尔表达式的短路翻译布尔表达式a b和a || b需要短路求值。a b的语义是如果 a 为假整个表达式为假不计算 b。翻译成四元式就是计算 a → 如果 a 为假跳到假出口 → 计算 b → 如果 b 为假跳到假出口 → 否则为真。def gen_and(self, left_false_list, right_false_list): # left_false_list 是左操作数为假时需要回填的四元式列表 # 右操作数的假出口和左操作数的假出口合并 return left_false_list right_false_list def gen_or(self, left_true_list, right_true_list): return left_true_list right_true_list这里返回的是回填列表的合并。实际实现时你需要为每个布尔表达式维护两个列表真出口列表和假出口列表。gen_and的假出口是左右假出口的并集真出口是右操作数的真出口。gen_or对称处理。提示短路翻译是实验里最容易出bug的地方建议先用简单表达式如a b手动推演一遍四元式序列确认跳转逻辑正确后再写代码。4. 把LR分析表和语义动作接起来实验代码的组织方式4.1 实验代码的模块划分天津理工这个实验通常给的是一个框架里面可能已经有词法分析器和LR分析表。你需要做的是在框架里补上语义分析和四元式生成模块。我一般会这样组织代码模块职责输入输出词法分析器字符流→Token流源程序Token列表LR分析器Token流→归约序列Token列表归约动作序列语义分析器归约动作→符号表操作归约动作符号表状态四元式生成器归约动作→四元式归约动作四元式列表错误处理器收集语义错误错误信息错误报告语义分析和四元式生成可以合并在一个模块里因为它们共享语义栈和符号表。关键是归约动作的分发每个产生式编号对应一个处理函数处理函数里同时做语义检查和四元式生成。4.2 归约动作的分发实现class SemanticAnalyzer: def __init__(self): self.symbol_table SymbolTable() self.quad_gen QuadGenerator() self.semantic_stack [] def on_reduce(self, prod_id): handler getattr(self, freduce_{prod_id}, None) if handler: handler() else: # 默认行为弹出右部符号数压入占位符 pass def reduce_1(self): # Assign - id Expr expr_type, expr_place self.semantic_stack.pop() self.semantic_stack.pop() # 弹出 id_name self.semantic_stack.pop() id_type self.symbol_table.lookup(id_name) if not self.type_compatible(id_type, expr_type): raise SemanticError(f类型不匹配: {id_name}) self.quad_gen.emit(, expr_place, -, id_name) self.semantic_stack.append((id_type, id_name))on_reduce根据产生式编号动态调用reduce_N方法。每个reduce_N方法从语义栈弹出右部符号的语义值执行检查和生成再把左部符号的语义值压回去。参数说明prod_id是产生式编号需要和你的语法定义文件对应semantic_stack里存的是元组(type, place)type用于类型检查place用于四元式生成。4.3 错误恢复与继续分析语义分析遇到错误时不应该直接崩溃退出而应该记录错误并继续分析尽可能多地发现错误。常见做法是遇到错误时在符号表里插入一个错误类型占位继续后续分析最后统一报告所有错误。class SemanticError(Exception): pass def safe_lookup(self, name): try: return self.symbol_table.lookup(name) except SemanticError as e: self.errors.append(str(e)) return (error, name) # 返回错误类型占位safe_lookup捕获查找异常记录错误返回一个错误类型占位符。这样后续的类型检查不会因为找不到变量而再次报错避免错误级联。参数errors是一个列表收集所有语义错误信息。注意错误恢复策略会影响实验评分有些老师要求遇到第一个错误就停止有些要求尽可能多报错。先确认实验要求再决定策略。5. 避坑指南语义分析与四元式生成里最容易翻车的五个点5.1 符号表作用域没有正确嵌套现象在 if 分支里声明的变量出了分支还能被访问到或者函数参数在函数体外可见。原因符号表只有一层没有实现作用域的压栈和弹栈。或者enter_scope和exit_scope的调用时机和语法结构不对应。解决用栈式符号表确保每个块结构if、while、函数体在进入时压栈、退出时弹栈。可以在语法分析的回调里做这件事比如归约到Block - { StmtList }时处理{时 enter_scope处理}时 exit_scope。5.2 四元式临时变量命名冲突现象生成的多个四元式使用了相同的临时变量名导致后续优化或解释执行时结果错误。原因临时变量计数器没有全局唯一或者在不同函数/作用域里重置了计数器。解决临时变量计数器应该是全局唯一的整个编译过程只增不减。如果实验要求分函数生成四元式可以在临时变量名前加函数前缀如func1_t1。5.3 控制流回填地址错误现象if 或 while 语句生成的跳转四元式目标地址不对程序执行时跳到了错误的位置。原因回填时四元式编号计算错误或者回填列表管理混乱。常见的是把四元式在列表中的下标和四元式的序号搞混了。解决统一用四元式在列表中的下标作为跳转目标。每次emit返回当前四元式的下标回填时直接把这个下标写入目标字段。建议写一个next_quad()函数返回下一个四元式的下标避免手动计算。5.4 类型检查过于严格或过于宽松现象合法的程序被报类型错误或者非法的类型赋值没有被检查出来。原因类型兼容规则没有按照实验要求实现。比如 int 和 float 的隐式转换规则不同实验要求可能不同。解决先确认实验的类型系统要求。常见规则是int 可以赋给 floatfloat 不能赋给 int除非显式转换算术运算中 int 和 float 混合时结果为 float。把这些规则写成一个type_compatible函数集中管理。5.5 布尔表达式短路翻译的出口列表合并错误现象a b || c这类混合布尔表达式生成的跳转逻辑错误短路行为不符合预期。原因真出口列表和假出口列表在合并时搞反了或者没有正确传递回填列表。解决画一棵布尔表达式的语法树手动标注每个节点的真出口和假出口然后按照树结构自底向上合并。的假出口是左右假出口的并集真出口是右真出口||的真出口是左右真出口的并集假出口是右假出口。写代码前先用纸笔推演一遍。6. 进阶技巧用四元式解释器验证你的生成结果写完语义分析和四元式生成之后怎么验证生成的四元式是对的最直接的办法是写一个四元式解释器把生成的四元式序列执行一遍看结果和预期是否一致。这个技巧在实验验收时特别有用因为老师通常会给你几个测试用例你可以在本地先跑一遍。class QuadInterpreter: def __init__(self, quads): self.quads quads self.vars {} self.pc 0 def run(self): while self.pc len(self.quads): op, arg1, arg2, result self.quads[self.pc] if op : self.vars[result] self.get_val(arg1) self.get_val(arg2) elif op -: self.vars[result] self.get_val(arg1) - self.get_val(arg2) elif op *: self.vars[result] self.get_val(arg1) * self.get_val(arg2) elif op /: divisor self.get_val(arg2) if divisor 0: raise RuntimeError(除零错误) self.vars[result] self.get_val(arg1) / divisor elif op : self.vars[result] self.get_val(arg1) elif op jz: if self.get_val(arg1) 0: self.pc result continue elif op j: self.pc result continue elif op jgt: if self.get_val(arg1) self.get_val(arg2): self.pc result continue self.pc 1 def get_val(self, arg): if isinstance(arg, (int, float)): return arg if arg in self.vars: return self.vars[arg] try: return float(arg) except ValueError: raise RuntimeError(f未定义变量: {arg})这个解释器支持基本的算术运算、赋值、条件跳转和无条件跳转。pc是程序计数器指向当前执行的四元式下标。jz在条件为假时跳转j无条件跳转jgt在大于时跳转。get_val负责把操作数转成数值如果是变量名就从vars里取如果是字面量就直接转换。用这个解释器你可以把实验的测试用例跑一遍对比输出结果。如果结果不对就回到四元式列表里逐条检查看是哪一步生成错了。我一般会先跑一个最简单的赋值语句再跑一个 if-else最后跑一个 while 循环逐步增加复杂度。还有一个技巧在生成四元式时顺便把每条四元式对应的源代码行号记录下来。这样解释器报错时你能直接定位到源代码的哪一行出了问题。这个信息在调试时非常有用相当于给四元式加了一个黑匣子。def emit_with_line(self, op, arg1, arg2, result, line): self.quads.append((op, arg1, arg2, result, line))把行号作为第五个字段存进去解释器执行时可以打印当前行号。这个改动很小但调试效率提升明显。最后说一个我自己的习惯每次改完语义动作先不跑完整测试而是用print把四元式列表打出来肉眼扫一遍。很多错误比如临时变量重复、跳转目标明显不对肉眼就能发现比跑解释器还快。等肉眼检查没问题了再用解释器跑测试用例。这个习惯帮我省了很多时间希望帮到你。本文还有配套的精品资源点击获取
返回列表