news 2026/10/17 2:54:07

编译原理实验代码:从词法分析到目标代码生成的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
编译原理实验代码:从词法分析到目标代码生成的完整实现

简介:这份资源是电子科技大学编译原理课程的实验代码合集,面向正在学习编译原理、需要完成词法分析与语法分析实验的高校学生及自学者。包内共21个文件,以cpp与h源码为主,辅以pas示例、vcxproj与sln工程文件、docx运行说明及exe可执行程序,压缩包约203KB,结构紧凑便于直接编译调试。代码完整覆盖词法分析器与语法分析器两大核心模块:词法部分借助正则或有限状态自动机将源码切分为token流,语法部分按既定规则解析token并构建抽象语法树,同时包含输入处理、语义检查与代码生成等环节的工程组织思路。已有2112人学习下载,说明其作为满分实验参考具有较高认可度。读者可据此对照运行说明复现实验流程,理解编译器前端各阶段的衔接方式,并借鉴工程文件与模块划分方法,为课程实验与后续编译器学习提供扎实的实践基础。

1. 编译原理实验代码:从词法分析到目标代码生成的完整实现

如果你正在上编译原理课,或者自学龙书但苦于理论落不了地,这套实验代码值得你花时间跑一遍。它把词法分析、语法分析、语义分析、中间代码生成、目标代码生成串成了一条完整的编译流水线,每个阶段都有独立可运行的模块和测试用例。我见过太多人学完编译原理只记得几个概念名词,真让他写个递归下降分析器就卡壳了。这套代码的价值在于:它把课本上那些抽象的状态转换图、文法推导、语法制导翻译,变成了你能单步调试、能改、能验证的工程代码。适合计算机专业学生做课设参考,也适合想补编译原理实践短板的开发者。

2. 项目结构拆解:五个阶段模块如何协同工作

2.1 目录组织与模块划分

拿到源码包后,先别急着跑。花五分钟把目录结构看清楚,后面改代码能省不少时间。这套实验代码的目录组织遵循编译阶段的自然划分,每个阶段一个独立目录,阶段之间通过中间表示文件传递数据。

compiler-lab/ ├── lexer/ # 词法分析模块 │ ├── lexer.py # 词法分析器主逻辑 │ ├── token_types.py # Token 类型定义 │ └── test_cases/ # 词法测试用例 ├── parser/ # 语法分析模块 │ ├── parser.py # 递归下降分析器 │ ├── grammar.py # 文法规则定义 │ └── ast_nodes.py # AST 节点定义 ├── semantic/ # 语义分析模块 │ ├── analyzer.py # 符号表管理与类型检查 │ └── symbol_table.py # 符号表数据结构 ├── ir/ # 中间代码生成 │ ├── ir_generator.py # 三地址码生成 │ └── ir_types.py # 中间代码指令定义 ├── codegen/ # 目标代码生成 │ ├── codegen.py # 汇编代码生成 │ └── register_alloc.py # 寄存器分配策略 ├── tests/ # 集成测试 │ └── test_pipeline.py # 端到端编译流程测试 ├── examples/ # 示例源程序 │ ├── factorial.src # 阶乘计算 │ └── fibonacci.src # 斐波那契数列 └── main.py # 编译入口,串联所有阶段

这个结构的好处是每个阶段可以独立测试。比如你只想验证词法分析器,直接跑lexer/下的测试用例就行,不用管后面的阶段。常见做法是每个模块都提供一个run()或parse()接口,输入输出都是明确定义的数据结构,这样阶段之间耦合度低,改一个阶段不容易把其他阶段搞崩。

2.2 编译流水线的数据流

理解数据怎么在阶段之间流动,比死记每个阶段的算法更重要。这套代码的数据流是这样的:

# main.py 中的编译主流程 def compile_source(source_code): # 阶段1:词法分析,源码字符串 -> Token 列表 tokens = lexer.tokenize(source_code) # 阶段2:语法分析,Token 列表 -> 抽象语法树 ast = parser.parse(tokens) # 阶段3:语义分析,AST -> 带类型标注的 AST + 符号表 annotated_ast, symbol_table = semantic.analyze(ast) # 阶段4:中间代码生成,标注 AST -> 三地址码序列 ir_code = ir_generator.generate(annotated_ast, symbol_table) # 阶段5:目标代码生成,三地址码 -> 汇编指令 assembly = codegen.generate(ir_code) return assembly

每个阶段的输出都是下一个阶段的输入,接口清晰。调试的时候可以在任意阶段打断点,把中间结果打印出来看。我一般会在每个阶段结束后加一行print或者写个 dump 函数,把当前阶段的输出格式化输出到文件,这样出问题能快速定位是哪个阶段翻车了。

提示:如果你拿到的源码包里没有main.py,可以自己写一个,把五个阶段的调用串起来就行,核心逻辑都在各模块内部。

2.3 各阶段核心接口说明

每个模块对外暴露的接口都很简洁,这是有意为之的设计。词法分析器只暴露tokenize(source)一个函数,输入字符串返回 Token 列表。语法分析器只暴露parse(tokens),输入 Token 列表返回 AST 根节点。语义分析暴露analyze(ast),返回标注后的 AST 和符号表。中间代码生成暴露generate(ast, symbol_table),返回指令列表。目标代码生成暴露generate(ir_code),返回汇编字符串。

这种设计的好处是你可以在任何阶段替换实现。比如你觉得递归下降太慢,想换成 LL(1) 表驱动分析器,只要新的 parser 模块也提供parse(tokens)接口,上层代码完全不用改。这就是模块化设计的价值,也是这套实验代码值得细看的地方。

3. 核心模块实现:词法分析与语法分析的代码细节

3.1 词法分析器:有限状态机的工程实现

词法分析的本质是把字符流切分成有意义的 Token 流。这套代码用的是手写有限状态机的方式,比正则表达式更可控,也更容易调试。核心逻辑在一个while循环里,根据当前字符决定状态转移。

# lexer/lexer.py 核心片段 def tokenize(source): tokens = [] pos = 0 line = 1 while pos < len(source): ch = source[pos] # 跳过空白字符,同时维护行号 if ch in ' \t': pos += 1 continue if ch == '\n': line += 1 pos += 1 continue # 识别标识符和关键字 if ch.isalpha() or ch == '_': start = pos while pos < len(source) and (source[pos].isalnum() or source[pos] == '_'): pos += 1 word = source[start:pos] # 查关键字表,决定是关键字还是普通标识符 if word in KEYWORDS: tokens.append(Token('KEYWORD', word, line)) else: tokens.append(Token('IDENTIFIER', word, line)) continue # 识别数字常量 if ch.isdigit(): start = pos while pos < len(source) and source[pos].isdigit(): pos += 1 tokens.append(Token('NUMBER', source[start:pos], line)) continue # 识别运算符和界符 if ch in OPERATORS: # 处理双字符运算符,如 ==, !=, <=, >= if pos + 1 < len(source) and source[pos:pos+2] in DOUBLE_OPS: tokens.append(Token('OPERATOR', source[pos:pos+2], line)) pos += 2 else: tokens.append(Token('OPERATOR', ch, line)) pos += 1 continue # 无法识别的字符,报错 raise LexerError(f"Unexpected character '{ch}' at line {line}") return tokens

这段代码的关键点有三个。第一,行号维护很重要,后面语法分析和语义分析报错时都需要行号来定位问题。第二,关键字和标识符的区分靠查表,KEYWORDS是一个集合,查找时间复杂度 O(1)。第三,双字符运算符的处理要优先于单字符,否则==会被拆成两个=。

参数方面,source是源程序字符串,返回值是Token对象列表。每个Token包含类型、值和行号三个字段。如果你想扩展语言支持,比如加一个+=运算符,只需要在DOUBLE_OPS集合里加上它,然后在语法分析器里加对应的文法规则就行。

3.2 语法分析器:递归下降构建 AST

语法分析用的是递归下降法,每个非终结符对应一个解析函数。这套代码支持的文法包括表达式、赋值语句、if 语句、while 语句和函数定义。核心思路是:遇到什么 Token 就调用对应的解析函数,函数内部递归调用其他解析函数。

# parser/parser.py 核心片段 class Parser: def __init__(self, tokens): self.tokens = tokens self.pos = 0 def peek(self): """查看当前 Token,不移动位置""" if self.pos < len(self.tokens): return self.tokens[self.pos] return None def consume(self, expected_type=None): """消费当前 Token 并前进""" token = self.peek() if token is None: raise ParseError("Unexpected end of input") if expected_type and token.type != expected_type: raise ParseError(f"Expected {expected_type}, got {token.type} at line {token.line}") self.pos += 1 return token def parse_expression(self): """解析表达式,处理运算符优先级""" left = self.parse_term() while self.peek() and self.peek().value in ('+', '-'): op = self.consume().value right = self.parse_term() left = BinaryOp(op, left, right) return left def parse_term(self): """解析项,处理乘除优先级""" left = self.parse_factor() while self.peek() and self.peek().value in ('*', '/'): op = self.consume().value right = self.parse_factor() left = BinaryOp(op, left, right) return left def parse_factor(self): """解析因子,处理括号和原子表达式""" token = self.peek() if token.type == 'NUMBER': self.consume() return NumberLiteral(int(token.value)) elif token.type == 'IDENTIFIER': self.consume() return Identifier(token.value) elif token.value == '(': self.consume() expr = self.parse_expression() self.consume('OPERATOR') # 期望右括号 return expr else: raise ParseError(f"Unexpected token {token.value} at line {token.line}")

递归下降的精髓在于用函数调用栈来模拟文法推导过程。parse_expression处理加减,parse_term处理乘除,parse_factor处理括号和原子表达式,三层调用自然实现了运算符优先级。这种写法比用 yacc/bison 生成分析器更直观,调试也方便,缺点是文法有左递归时需要改写。

参数说明:tokens是词法分析输出的 Token 列表,pos是当前解析位置指针。每个parse_xxx函数返回对应的 AST 节点。如果你想加新的语法结构,比如for循环,就在 Parser 类里加一个parse_for_statement方法,然后在语句解析的分发逻辑里注册它。

3.3 语义分析:符号表与类型检查

语义分析阶段做两件事:建符号表和作用域检查,以及类型检查。符号表用栈式结构管理作用域,进入一个块就压栈,离开就弹栈。

# semantic/symbol_table.py 核心片段 class SymbolTable: def __init__(self): self.scopes = [{}] # 栈底是全局作用域 def enter_scope(self): """进入新作用域""" self.scopes.append({}) def exit_scope(self): """退出当前作用域""" if len(self.scopes) > 1: self.scopes.pop() def declare(self, name, type_info): """在当前作用域声明变量""" current_scope = self.scopes[-1] if name in current_scope: raise SemanticError(f"Variable '{name}' already declared in this scope") current_scope[name] = type_info def lookup(self, name): """从内到外查找变量""" for scope in reversed(self.scopes): if name in scope: return scope[name] raise SemanticError(f"Undeclared variable '{name}'")

符号表的设计直接影响语义分析的准确性。栈式结构天然支持嵌套作用域,lookup从最内层往外找,符合大多数语言的变量查找规则。declare检查重复声明,防止同一作用域内变量名冲突。

类型检查在遍历 AST 时进行,每个表达式节点计算出一个类型,赋值语句检查左右类型是否兼容。常见做法是用一个type_of函数递归计算表达式类型,遇到不兼容就报错。这套代码支持int和float两种基本类型,以及它们的隐式转换规则。

4. 运行与调试:从源码到汇编的完整验证

4.1 环境准备与依赖安装

这套代码用 Python 实现,依赖很少,标准库基本够用。建议用 Python 3.8 以上版本,避免一些语法兼容性问题。

# 创建虚拟环境(推荐,避免污染全局包) python3 -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate # Windows # 安装测试依赖(如果有 requirements.txt) pip install -r requirements.txt # 如果没有 requirements.txt,手动装 pytest 用于跑测试 pip install pytest

环境准备好后,先跑一遍测试用例,确认代码能正常工作。这一步很关键,能帮你排除环境问题导致的玄学错误。

# 跑所有测试 pytest tests/ -v # 只跑词法分析测试 pytest lexer/test_cases/ -v # 跑端到端集成测试 pytest tests/test_pipeline.py -v

如果测试全绿,说明环境没问题,可以开始跑示例程序了。

4.2 编译一个示例程序

拿examples/factorial.src举例,这是一个计算阶乘的源程序。用main.py编译它,观察每个阶段的输出。

# 编译阶乘示例,输出汇编代码 python main.py examples/factorial.src # 如果想看中间结果,加 --dump-ir 参数 python main.py examples/factorial.src --dump-ir # 输出词法分析结果 python main.py examples/factorial.src --dump-tokens

编译流程走完后,你会看到生成的汇编代码。这套代码生成的是简化的类 MIPS 汇编,每条指令对应一个基本操作。如果你想把汇编跑起来,可以用 SPIM 或 MARS 模拟器加载执行。常见做法是在codegen模块里加一个--target参数,支持生成不同架构的汇编,但实验代码一般只做一种。

注意:如果编译时报Undeclared variable错误,先检查源程序里变量是不是没声明就用了。语义分析阶段对未声明变量是零容忍的。

4.3 分阶段调试技巧

调试编译器最怕的就是一跑就报错,但不知道错在哪个阶段。我的习惯是每个阶段单独跑,确认输出符合预期后再串起来。

# debug_pipeline.py 分阶段调试脚本 from lexer.lexer import tokenize from parser.parser import parse from semantic.analyzer import analyze from ir.ir_generator import generate from codegen.codegen import generate_assembly source = open('examples/factorial.src').read() # 阶段1:词法分析 tokens = tokenize(source) print("=== Tokens ===") for t in tokens: print(f" {t.type}: {t.value} (line {t.line})") # 阶段2:语法分析 ast = parse(tokens) print("\n=== AST ===") print(ast) # 假设 AST 节点实现了 __repr__ # 阶段3:语义分析 annotated_ast, symtab = analyze(ast) print("\n=== Symbol Table ===") print(symtab) # 阶段4:中间代码 ir_code = generate(annotated_ast, symtab) print("\n=== IR ===") for instr in ir_code: print(f" {instr}") # 阶段5:目标代码 asm = generate_assembly(ir_code) print("\n=== Assembly ===") print(asm)

这个脚本把每个阶段的中间结果都打印出来,哪一步输出不对一眼就能看出来。比如 Token 列表里少了某个标识符,那就是词法分析的问题;AST 结构不对,那就是语法分析的问题。这种分阶段验证的方法能帮你快速定位 bug,比在完整流程里瞎猜高效得多。

5. 避坑与常见问题:编译实验中的血泪经验

5.1 词法分析阶段的行号丢失

现象:语法分析报错时提示的行号全是 1,根本定位不到源程序哪一行出了问题。

原因:词法分析器在跳过换行符时忘了递增行号计数器,或者 Token 创建时没把当前行号传进去。

解决:在词法分析器的空白字符处理分支里,遇到\n必须line += 1。创建 Token 时统一用当前line变量。建议写个单元测试专门验证多行源程序的行号是否正确。

5.2 递归下降分析器的左递归死循环

现象:语法分析器一跑就栈溢出,或者卡死不动。

原因:文法里存在左递归,比如expr -> expr + term,递归下降分析器会无限递归调用自己。

解决:把左递归改写成右递归,或者用循环替代递归。比如expr -> term (('+' | '-') term)*,在代码里用while循环处理运算符,而不是递归调用parse_expression。

5.3 符号表作用域泄漏

现象:在一个代码块里声明的变量,出了块还能访问,或者同名变量在不同块里报重复声明。

原因:进入块时忘了enter_scope(),或者退出块时忘了exit_scope(),导致作用域栈没有正确维护。

解决:在语法分析器解析块语句时,进入块前调用enter_scope(),解析完块后调用exit_scope()。建议用try/finally确保异常时也能正确退出作用域。

5.4 中间代码生成的临时变量命名冲突

现象:生成的三地址码里,不同表达式的临时变量用了同一个名字,导致计算结果互相覆盖。

原因:临时变量计数器没有全局唯一,或者每次生成新表达式时重置了计数器。

解决:用一个全局递增的计数器生成临时变量名,如t1、t2、t3,确保整个编译过程中不重复。常见做法是在 IR 生成器类里维护一个self.temp_count实例变量。

5.5 目标代码生成的寄存器分配错误

现象:生成的汇编代码运行结果不对,但中间代码看起来没问题。

原因:寄存器分配时没有正确处理活跃变量,导致两个同时活跃的变量被分配到了同一个寄存器。

解决:实验代码一般用简单的寄存器分配策略,比如所有变量都放栈上,需要计算时才加载到寄存器。如果要做寄存器分配优化,先实现活跃变量分析,确保不冲突再分配。

6. 二次开发指南:扩展语言特性与优化编译流程

6.1 添加新语法结构的完整流程

假设你想给这套编译器加一个for循环支持,需要改四个地方。第一步,在词法分析器的关键字表里加上for。第二步,在语法分析器里加parse_for_statement方法,解析for (init; cond; update) body结构。第三步,在语义分析器里处理for节点的作用域和类型检查。第四步,在中间代码生成器里把for循环翻译成条件跳转和无条件跳转的组合。

# parser/parser.py 中添加 for 语句解析 def parse_for_statement(self): """解析 for 循环: for (init; cond; update) body""" self.consume('KEYWORD') # 消费 'for' self.consume('OPERATOR') # 消费 '(' init = self.parse_statement() # 初始化语句 self.consume('OPERATOR') # 消费 ';' cond = self.parse_expression() # 循环条件 self.consume('OPERATOR') # 消费 ';' update = self.parse_statement() # 更新语句 self.consume('OPERATOR') # 消费 ')' body = self.parse_block() # 循环体 return ForNode(init, cond, update, body)

这个解析函数返回一个ForNode,包含初始化、条件、更新和循环体四个子节点。语义分析时,ForNode会创建一个新作用域,初始化语句在这个作用域里声明变量。中间代码生成时,ForNode被翻译成标签和跳转指令的组合,这是编译原理里控制流翻译的标准做法。

6.2 优化中间代码:常量折叠与死代码消除

实验代码一般不做优化,但你可以自己加上去。常量折叠是最简单的优化,在中间代码生成阶段,如果发现两个操作数都是常量,直接计算结果替换掉原来的指令。

# ir/optimizer.py 常量折叠示例 def constant_folding(ir_code): """遍历中间代码,折叠常量表达式""" optimized = [] for instr in ir_code: if instr.op == 'ADD' and is_constant(instr.arg1) and is_constant(instr.arg2): # 两个操作数都是常量,直接计算结果 result = int(instr.arg1) + int(instr.arg2) optimized.append(IRInstruction('ASSIGN', instr.result, str(result))) elif instr.op == 'MUL' and is_constant(instr.arg1) and is_constant(instr.arg2): result = int(instr.arg1) * int(instr.arg2) optimized.append(IRInstruction('ASSIGN', instr.result, str(result))) else: optimized.append(instr) return optimized

常量折叠能减少运行时的计算量,虽然对实验代码来说性能提升有限,但这是理解编译优化的好起点。死代码消除稍微复杂一点,需要先做活跃变量分析,找出那些计算结果永远不会被使用的指令,然后删掉它们。

6.3 验证改造是否正确的测试方法

改完编译器后,怎么确认没改坏?我的做法是准备一组回归测试用例,覆盖所有已有语法结构和边界情况。每次改完代码,先跑回归测试,确保已有功能没退化,再跑新功能的测试用例。

# 回归测试脚本 #!/bin/bash # 遍历所有示例程序,编译并对比输出 for src in examples/*.src; do echo "Testing $src..." python main.py "$src" > /tmp/output.asm 2>&1 if [ $? -ne 0 ]; then echo "FAIL: $src compilation error" continue fi # 对比预期输出(如果有的话) expected="${src%.src}.expected" if [ -f "$expected" ]; then diff /tmp/output.asm "$expected" && echo "PASS" || echo "FAIL: output mismatch" else echo "PASS (no expected output)" fi done

这个脚本遍历examples/下所有源程序,编译后对比预期输出。如果你改了词法或语法分析器,这个脚本能快速告诉你有没有把原来的功能改坏。我一般会在改代码前先跑一遍,确认全绿,改完再跑一遍,对比结果。

从那以后我每次改编译器代码,都强制走一遍「回归测试 → 新功能测试 → 边界测试」的流程,再也不敢跳过任何一步。希望这套实验代码和上面的改造思路能帮到你,少走一些我当年踩过的弯路。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/17 2:52:46

Ionic+Cordova插件集成Unity:让AR功能成为原生模块

简介&#xff1a;UnityIonicIntegration 是一份针对 Unity 3D 内容与 Ionic 混合应用双向集成的实战指南&#xff0c;适合需要同时掌握 Android/iOS 端插件封装与原生通信的移动开发者。方案基于 Cordova 插件实现 Unity 与 Ionic 之间双向消息传递&#xff0c;并已在带 Vufori…

作者头像 李华
网站建设 2026/10/17 2:52:44

labelImg图像标注工具详解:VOC格式转YOLO的完整流程与避坑指南

简介&#xff1a;LabelImg 是一款开源的可视化图像标注工具&#xff0c;主要面向目标检测领域的数据集制作环节。使用者可以通过图形界面快速框选图像中的目标&#xff0c;并自动生成符合 PASCAL VOC 规范的 XML 标注文件&#xff0c;为 Faster R-CNN、YOLO、SSD 等主流检测网络…

作者头像 李华
网站建设 2026/10/17 2:52:33

OpenCV C/C++动态抠图源码实战:从背景建模到掩膜合成的完整链路

简介&#xff1a;本资源面向计算机视觉初学者与C/C开发者&#xff0c;提供基于OpenCV的动态扣图完整源代码&#xff0c;解决实时视频流或连续图像序列中前景目标提取的问题。代码围绕背景减除模型展开&#xff0c;涵盖BackgroundSubtractorMOG2与BackgroundSubtractorKNN两种主…

作者头像 李华
网站建设 2026/10/17 2:51:56

地球微观粒子不能推导出宇宙宏观| 赵杰 | 量子意识(第三卷)

作者&#xff1a;赵杰清华大学硕士、微美全息云科技&#xff08;NASDAQ&#xff1a;WIMI&#xff09;董事长、微算法科技&#xff08;NASDAQ&#xff1a;MLGO&#xff09;董事长、育杰奖学金创始人宇宙的尺度层级中&#xff0c;地球微观粒子的研究始终局限于 “局部可控的实验室…

作者头像 李华
网站建设 2026/10/17 2:50:59

Cookie反爬绕过

Cookie反爬 cookie反爬虫的基本概念Cookie反爬虫是网站用来区分正常用户个爬虫程序的一种技术手段&#xff0c;通过验证请求中的Cookie值来阻止恶意爬取。服务器会验证Cookie的合法性&#xff0c;若Cookie不存在&#xff0c;无效或过期&#xff0c;服务器将拒绝返回真实数据&am…

作者头像 李华