☰
天津理工大学编译原理实验3:语义分析与中间代码生成实战指南
2026/10/3 6:20:34 网站建设 项目流程

简介:本资源为天津理工大学编译原理实验三的完整实验报告,面向计算机与通信工程学院修读编译原理课程的学生,聚焦语义分析与中间代码生成这一核心环节。报告以文法 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、jnz;arg1和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字典初始包含所有输入变量的值,临时变量在执行过程中动态加入。注意,除法这里没处理除零,实际用的时候加个判断。这个解释器只有几十行,但能帮你快速定位四元式生成中的逻辑错误,比如跳转目标错了、临时变量没赋值等。

我自己的习惯是:每生成完一个测试用例的四元式,先肉眼扫一遍跳转目标,再跑解释器对答案。如果解释器结果和预期不符,就在解释器里加打印,看是哪条四元式开始偏的。这个笨办法帮我省了无数个熬夜调回填的晚上。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询