
简介这份资源是中国海洋大学2020年春季学期编译原理课程的完整实验代码合集面向正在学习编译原理、需要动手实践编译器构造的高校学生与自学者。包内共74个文件以C语言源码、Flex词法规则文件.l、Bison语法规则文件.y、头文件、Makefile及可执行程序为主另含实验要求文档与测试用例压缩包约774KB覆盖从词法分析、语法分析、语义分析到中间代码生成、代码优化、目标代码生成、错误处理与编译器综合的八个实验环节。每个实验均配有源码与说明可对照文法规则、产生式与测试输入逐步调试理解标记流、语法树、符号表、三地址码等核心概念并熟悉ANTLR、Flex、Bison等工具的实际用法。目前已有4411人学习下载适合作为课程作业参考与编译器实现练手素材。1. OUC编译原理全部实验从词法分析到代码生成的完整通关路径如果你正在搜“OUC编译原理全部实验”大概率是两种情况要么课设临近截止你手里只有一个语法制导翻译的模糊概念却要交出一套能跑通词法、语法、语义、四元式生成全流程的代码要么你已经翻过一遍《编译原理》清华大学出版社第三版的课后答案发现答案能看懂但真让你从零写一个能处理嵌套作用域和类型检查的编译器还是不知道从哪下手。OUC的编译原理实验通常不是一次大作业而是拆成词法分析、语法分析、语义分析与中间代码生成、目标代码生成几个递进模块每个模块单独验收最后串成一条完整链路。这篇文章不讲虚的我按自己带学生和复盘项目的经验把每个实验的最小可运行实现、参数怎么调、哪里最容易翻车一层层拆开。适合刚学完理论、需要动手落地的人也适合已经写过一部分但卡在某个环节的熟手。2. 词法分析实验用状态机把字符流切成Token序列2.1 为什么先写DFA而不是直接上正则库很多同学第一反应是用Python的re模块一把梭但OUC实验的验收点往往在“你能否手动构造确定有限自动机DFA并处理最长匹配”。直接调库虽然能出结果但答辩时老师问一句“你的状态转移表在哪”就露馅了。我一般会先画一张状态转移图把标识符、关键字、整数、浮点数、运算符、界符这几类Token的识别路径理清楚再落成代码。核心逻辑是读入一个字符根据当前状态和字符类别跳到下一个状态如果下一个状态是终态且再读一个字符会跳出就回退并输出当前Token。这里的关键参数是“回退指针”和“关键字表”——关键字表用哈希或字典都行但一定要在识别出标识符后查表而不是在状态机里硬编码。2.2 手写词法分析器的完整代码与参数说明下面是一个能处理常见C语言子集词法的最小实现你可以直接复制到lexer.py里跑。输入是一个源文件路径输出是Token列表每个Token带类型、值和行号。# lexer.py import sys KEYWORDS {if, else, while, for, int, float, return, void} class Token: def __init__(self, type_, value, line): self.type type_ # 类型ID, NUM, KEYWORD, OP, DELIM, EOF self.value value # 原始字符串 self.line line # 行号用于报错 class Lexer: def __init__(self, text): self.text text self.pos 0 self.line 1 self.current_char self.text[self.pos] if self.text else None def advance(self): # 移动指针维护行号 if self.current_char \n: self.line 1 self.pos 1 if self.pos len(self.text): self.current_char self.text[self.pos] else: self.current_char None def skip_whitespace(self): while self.current_char is not None and self.current_char.isspace(): self.advance() def number(self): # 识别整数和浮点数支持小数点 result while self.current_char is not None and (self.current_char.isdigit() or self.current_char .): result self.current_char self.advance() # 简单校验不能有多个小数点 if result.count(.) 1: raise SyntaxError(fLine {self.line}: Invalid number {result}) return Token(NUM, result, self.line) def identifier(self): # 识别标识符或关键字 result while self.current_char is not None and (self.current_char.isalnum() or self.current_char _): result self.current_char self.advance() if result in KEYWORDS: return Token(KEYWORD, result, self.line) return Token(ID, result, self.line) def get_next_token(self): while self.current_char is not None: if self.current_char.isspace(): self.skip_whitespace() continue if self.current_char.isdigit(): return self.number() if self.current_char.isalpha() or self.current_char _: return self.identifier() # 双字符运算符优先匹配 if self.current_char and self.pos 1 len(self.text) and self.text[self.pos1] : self.advance(); self.advance() return Token(OP, , self.line) if self.current_char in -*/!: op self.current_char self.advance() return Token(OP, op, self.line) if self.current_char in ;(){}[],: delim self.current_char self.advance() return Token(DELIM, delim, self.line) raise SyntaxError(fLine {self.line}: Unexpected character {self.current_char}) return Token(EOF, , self.line) if __name__ __main__: with open(sys.argv[1], r) as f: source f.read() lexer Lexer(source) tokens [] while True: tok lexer.get_next_token() tokens.append(tok) if tok.type EOF: break for t in tokens: print(f{t.type:8} {t.value:12} line {t.line})这段代码的逻辑说明advance负责推进指针并维护行号skip_whitespace跳过空白number和identifier分别处理数字和字母开头的串。参数上KEYWORDS集合决定了哪些标识符会被提升为关键字你可以按实验要求增删。双字符运算符如必须在单字符之前判断否则会被拆成两个。运行命令是python lexer.py test.c输出每行一个Token。如果遇到非法字符会抛出带行号的异常方便定位。2.3 词法分析验收时老师常问的三个边界第一个边界是“最长匹配”比如不能识别成两个123.45.6要报错而不是切成两个数。第二个边界是“关键字与标识符的区分”int是关键字intx是标识符查表时机必须在完整读出单词之后。第三个边界是“行号维护”多行注释或字符串跨行时行号要正确递增否则后续语法报错定位会偏。我见过不少同学因为行号错位在语法分析阶段排查了半天血泪经验就是词法阶段一定要把行号打对。3. 语法分析实验用递归下降构造语法树并处理优先级3.1 递归下降 vs LR分析OUC实验怎么选OUC的语法分析实验通常允许两种方案手写递归下降分析器或者用Yacc/Bison生成LR分析表。如果你时间紧、语法规则不复杂比如只要求表达式和简单语句递归下降是最稳的因为代码结构直接对应文法产生式调试时能一眼看出哪条规则没匹配上。但如果你要处理完整的C语言子集包括嵌套if-else和数组声明LR分析器更省事只是需要额外学Bison的语法。我一般建议实验要求“能分析表达式和赋值语句”就用递归下降要求“能分析函数定义和复合语句”就上Bison。下面以递归下降为例因为它更能体现你对文法左递归消除和优先级处理的理解。3.2 表达式语法树的递归下降实现假设文法已经消除了左递归表达式优先级从低到高是加减、乘除、括号和一元负号。下面代码在词法分析器基础上增加语法分析输出一棵简单的AST。# parser.py from lexer import Lexer, Token class ASTNode: def __init__(self, type_, leftNone, rightNone, valueNone): self.type type_ # NUM, ID, BINOP, ASSIGN self.left left self.right right self.value value class Parser: def __init__(self, lexer): self.lexer lexer self.current_token self.lexer.get_next_token() def eat(self, token_type): # 消费当前Token如果类型不匹配则报错 if self.current_token.type token_type: self.current_token self.lexer.get_next_token() else: raise SyntaxError(fExpected {token_type}, got {self.current_token.type} at line {self.current_token.line}) def factor(self): # factor: NUM | ID | ( expr ) | - factor tok self.current_token if tok.type NUM: self.eat(NUM) return ASTNode(NUM, valuetok.value) elif tok.type ID: self.eat(ID) return ASTNode(ID, valuetok.value) elif tok.type DELIM and tok.value (: self.eat(DELIM) node self.expr() self.eat(DELIM) # 期望 ) return node elif tok.type OP and tok.value -: self.eat(OP) return ASTNode(BINOP, leftASTNode(NUM, value0), rightself.factor(), value-) else: raise SyntaxError(fUnexpected token {tok.type} at line {tok.line}) def term(self): # term: factor ((*|/) factor)* node self.factor() while self.current_token.type OP and self.current_token.value in (*, /): op self.current_token.value self.eat(OP) node ASTNode(BINOP, leftnode, rightself.factor(), valueop) return node def expr(self): # expr: term ((|-) term)* node self.term() while self.current_token.type OP and self.current_token.value in (, -): op self.current_token.value self.eat(OP) node ASTNode(BINOP, leftnode, rightself.term(), valueop) return node def parse(self): return self.expr() if __name__ __main__: import sys with open(sys.argv[1], r) as f: source f.read() lexer Lexer(source) parser Parser(lexer) ast parser.parse() print(Parse OK)逻辑说明factor处理最小单元term处理乘除expr处理加减这样自然实现了优先级。参数上eat函数负责匹配并前进如果类型不对就抛异常。注意一元负号的处理我把它转成0 - factor这样后续语义分析不用单独处理负数。运行python parser.py expr.c如果输出Parse OK就说明语法正确。你可以把AST打印出来验证结构比如12*3应该得到( 1 (* 2 3))。3.3 语法分析常见的左递归与优先级翻车点左递归是递归下降的致命伤比如expr - expr term会导致无限递归。消除方法是改成expr - term exprexpr - term expr | ε。我在代码里直接用循环替代了尾递归效果一样但更直观。另一个坑是优先级如果你把加减和乘除放在同一层12*3会算成(12)*3结果错误。必须严格按expr - term - factor分层。还有括号匹配eat(DELIM)时没有检查值是不是)如果源文件里是(就会误判建议在eat里增加值校验。4. 语义分析与中间代码生成用四元式把AST翻译成线性序列4.1 符号表设计与作用域嵌套的处理语义分析的核心是符号表。OUC实验通常要求支持全局变量和局部变量这意味着符号表要能嵌套。我一般用栈式符号表进入一个作用域就压入一个新表离开就弹出。每个表项记录变量名、类型、作用域层级和偏移量。类型检查时遇到赋值语句要检查左右类型是否兼容比如int float要报错或隐式转换。四元式的格式是(op, arg1, arg2, result)比如a b c生成(, b, c, t1)和(, t1, _, a)。临时变量用t1, t2...递增命名。4.2 从AST生成四元式的代码实现下面代码在AST基础上做后序遍历生成四元式列表。假设只处理整数和简单变量。# semantic.py from parser import Parser, ASTNode from lexer import Lexer class SymbolTable: def __init__(self): self.scopes [{}] # 栈式作用域初始全局 self.temp_count 0 def enter_scope(self): self.scopes.append({}) def exit_scope(self): self.scopes.pop() def declare(self, name, type_): # 在当前作用域声明变量重复声明报错 if name in self.scopes[-1]: raise SyntaxError(fVariable {name} already declared) self.scopes[-1][name] type_ def lookup(self, name): # 从内到外查找变量 for scope in reversed(self.scopes): if name in scope: return scope[name] raise SyntaxError(fUndeclared variable {name}) def new_temp(self): self.temp_count 1 return ft{self.temp_count} class QuadGenerator: def __init__(self): self.quads [] self.symtab SymbolTable() def generate(self, node): # 后序遍历AST返回存放结果的变量名或临时变量 if node.type NUM: return node.value elif node.type ID: self.symtab.lookup(node.value) # 检查是否声明 return node.value elif node.type BINOP: left self.generate(node.left) right self.generate(node.right) temp self.symtab.new_temp() self.quads.append((node.value, left, right, temp)) return temp else: raise SyntaxError(fUnknown node type {node.type}) if __name__ __main__: import sys with open(sys.argv[1], r) as f: source f.read() lexer Lexer(source) parser Parser(lexer) ast parser.parse() gen QuadGenerator() # 假设源文件第一行是变量声明这里手动声明a和b gen.symtab.declare(a, int) gen.symtab.declare(b, int) result gen.generate(ast) for q in gen.quads: print(f({q[0]}, {q[1]}, {q[2]}, {q[3]})) print(fResult: {result})逻辑说明generate递归处理AST遇到二元运算就先生成左右子表达式再创建临时变量并追加四元式。参数上SymbolTable的scopes列表模拟作用域栈lookup从最内层往外找。运行python semantic.py expr.c输入a b * 2输出应该是(*, b, 2, t1)和(, a, t1, t2)。注意临时变量编号是全局递增的不要每个作用域重置否则会冲突。4.3 类型检查与隐式转换的取舍类型检查是语义分析最容易过度设计的地方。OUC实验通常只要求检查“变量是否声明”和“赋值类型是否匹配”不会要求完整的类型推导。我一般只做两件事在lookup时确认变量存在在赋值时比较左右类型。如果实验要求支持float和int混合运算就在生成四元式前插入转换指令比如(int2float, t1, _, t2)。但注意隐式转换的规则要写清楚否则int float的结果类型会模棱两可。我的习惯是只要有一个操作数是float结果就是float整数自动提升。5. 目标代码生成与实验串联从四元式到可执行伪汇编5.1 四元式到栈式虚拟机的映射目标代码生成实验通常要求把四元式翻译成某种汇编或虚拟机指令。最简单的是栈式虚拟机每条四元式对应几条PUSH、POP、ADD等指令。比如(, a, b, t1)翻译成PUSH a、PUSH b、ADD、POP t1。寄存器分配是进阶内容如果实验不要求就用栈式方案代码量少且不容易出错。我一般会定义一个指令表每条指令带操作码和操作数然后顺序执行。5.2 生成伪汇编并验证执行结果下面代码把四元式列表转成栈式指令并模拟执行验证结果。# codegen.py class StackVM: def __init__(self): self.stack [] self.vars {} def execute(self, quads): for op, arg1, arg2, result in quads: if op : self.stack.append(self.vars.get(arg1, int(arg1)) self.vars.get(arg2, int(arg2))) elif op -: self.stack.append(self.vars.get(arg1, int(arg1)) - self.vars.get(arg2, int(arg2))) elif op *: self.stack.append(self.vars.get(arg1, int(arg1)) * self.vars.get(arg2, int(arg2))) elif op /: self.stack.append(self.vars.get(arg1, int(arg1)) // self.vars.get(arg2, int(arg2))) elif op : self.vars[result] self.stack.pop() # 临时变量直接存回vars if result and op ! : self.vars[result] self.stack.pop() return self.vars if __name__ __main__: # 假设四元式来自semantic.py的输出 quads [(*, b, 2, t1), (, a, t1, t2)] vm StackVM() vm.vars[a] 3 vm.vars[b] 4 result vm.execute(quads) print(ft2 {result.get(t2)}) # 应该输出11逻辑说明execute遍历四元式根据操作码做运算结果压栈或存变量。参数上vars字典模拟内存stack模拟运算栈。运行后t2应该是3 4*2 11。这个虚拟机很粗糙但足以验证四元式逻辑是否正确。如果你要生成真正的x86汇编就把每条四元式映射成mov、add、imul等指令但工作量会大很多建议先跑通栈式版本。5.3 把四个实验串成一条流水线的脚本单独跑每个模块没问题但验收时老师往往要求“输入一个源文件输出最终结果”。我一般写一个main.py按顺序调用词法、语法、语义、代码生成中间用管道传递数据。关键是要统一错误处理任何阶段抛异常都要打印行号和原因并终止后续步骤。另外临时变量编号和符号表要在整个流水线中共享不要每个模块重新初始化。下面是一个串联示例的伪代码结构# main.py from lexer import Lexer from parser import Parser from semantic import QuadGenerator from codegen import StackVM def compile(source_path): with open(source_path) as f: source f.read() lexer Lexer(source) parser Parser(lexer) ast parser.parse() gen QuadGenerator() # 这里需要根据源文件实际声明来填充符号表简化处理 gen.symtab.declare(a, int) gen.symtab.declare(b, int) gen.generate(ast) vm StackVM() vm.vars[a] 3 vm.vars[b] 4 vm.execute(gen.quads) return vm.vars if __name__ __main__: import sys result compile(sys.argv[1]) print(result)注意符号表填充在实际实验中应该由声明语句驱动这里为了演示手动写死了。你需要在语法分析里增加变量声明规则并在语义分析时调用declare。6. 避坑与排查OUC编译原理实验里最容易翻车的五个地方6.1 现象词法分析输出一堆UNKNOWN但源文件看起来没问题原因最常见的是不可见字符比如从网页复制代码时带了全角空格或BOM头。另一个可能是advance函数在文件末尾没有正确处理None导致越界。解决在Lexer初始化时用source source.replace(\u3000, ).replace(\ufeff, )清洗并在advance里加边界判断。如果还不行打印每个字符的ord()值看是不是有非ASCII字符混入。6.2 现象语法分析报“Expected DELIM, got OP”但括号明明匹配原因eat函数只检查了类型没检查值。比如(和)都是DELIM类型如果代码里写self.eat(DELIM)遇到(也会通过导致后续错位。解决把eat改成eat(token_type, valueNone)如果传了value就同时校验。或者在调用时手动判断self.current_token.value )。6.3 现象四元式生成时临时变量名重复导致结果覆盖原因temp_count定义在SymbolTable里但每次进入新作用域时如果重新创建了SymbolTable实例计数器就归零了。解决整个编译过程只用一个SymbolTable实例enter_scope和exit_scope只操作scopes列表不重置temp_count。另外临时变量名最好带前缀如t1避免和用户变量冲突。6.4 现象目标代码执行结果和预期差一位或者负数除法出错原因栈式虚拟机里/用了整数除法//如果实验要求浮点除法就会截断。另外操作数顺序可能反了比如a - b翻译成PUSH a, PUSH b, SUB但栈是后进先出SUB应该弹出b和a做a - b如果实现成b - a就错了。解决在execute里明确操作数顺序或者用self.stack.pop()两次并注意先后。负数除法建议用int(a / b)而不是//避免向下取整。6.5 现象串联运行时符号表报“Undeclared variable”但变量明明声明了原因声明语句在AST里可能被解析成了表达式没有触发declare调用。或者作用域进入/退出时机不对比如在解析函数体时没有enter_scope导致局部变量被声明到全局。解决在语法分析里为声明语句单独建一个AST节点类型然后在语义分析时优先处理。另外打印符号表当前内容确认变量在正确的scopes层级里。7. 进阶技巧用测试用例驱动开发把验收通过率提到九成如果你已经跑通了基本流程下一步不是继续加功能而是建一套测试用例。我习惯在项目根目录建tests/文件夹每个实验模块对应一个.c输入和.expected输出。比如词法分析用test_lexer_01.c里面故意混入关键字、浮点数、双字符运算符期望输出逐行比对。语法分析用test_parser_01.c覆盖加减乘除、括号、一元负号。语义分析用test_semantic_01.c包含未声明变量和类型不匹配期望抛出特定异常。目标代码用test_codegen_01.c输入固定值期望输出计算结果。跑测试的脚本可以用pytest也可以用最简单的bash循环。下面是一个run_tests.sh示例#!/bin/bash # run_tests.sh for input in tests/*.c; do name$(basename $input .c) expectedtests/${name}.expected actual$(python main.py $input 21) if [ $actual $(cat $expected) ]; then echo PASS: $name else echo FAIL: $name echo Expected: $(cat $expected) echo Actual: $actual fi done这个脚本会遍历tests/下所有.c文件用main.py编译并比对输出。参数上21把错误输出也捕获方便比对异常信息。我一般会先写10个用例覆盖正常和异常路径然后每修一个bug就加一个用例防止回归。这套方法让我在验收时基本没被问倒因为老师随机抽的输入我大概率已经覆盖了。最后一个习惯每次实验验收前把main.py从头到尾跑一遍确认没有硬编码的变量值比如我示例里的a3, b4所有数据都来自源文件解析。如果时间允许把符号表填充改成由声明语句驱动这样才算真正完整的编译器前端。希望帮到你。本文还有配套的精品资源点击获取