简介:本资源是东南大学网络安全学院《编译方法》课程设计实践包,面向计算机/网安专业本科生及编译原理初学者,聚焦词法分析、语法解析、语义处理与代码生成等核心环节的动手实现。压缩包含260个文件,总大小19.61MB,以55份Markdown实验说明文档为学习主线,辅以67个GraphML格式的语法树/控制流图可视化文件、73个GIF动图演示编译流程、8个DOT/SVG/PNG图表辅助理解结构,以及7个Java、5个C++、4个C语言源码文件(含lexical_analyzer.cpp、syntax_parser.cpp、main.cpp及多个.c测试用例),覆盖从Tokenizer到AST构建的完整前端实现。已有137人下载学习,资源结构清晰、理论与实操强耦合,提供可直接编译运行的工程(含sln/vcxproj项目文件)、详尽运行说明与调试指引,特别适合通过复现经典编译阶段来夯实原理认知、培养系统级编程能力。
1. 这不是一份“交作业式”课程设计:它是一套可跑通、可调试、可延伸的编译器前端实践闭环
你点开这个压缩包,看到“东南大学-网安学院-编译方法课程设计”,第一反应可能是:又一个学生交差用的LL(1)文法+手工递归下降+简单词法分析器?但实际打开后你会发现——它带完整 Makefile、支持 .c 文件输入、输出三地址码(TAC)中间表示、能用 Python 脚本可视化 AST、甚至附了 5 个覆盖不同语法特性的测试用例(含嵌套 if-else、while 循环、数组访问、函数调用雏形)。这不是玩具,是网安学院在“编译原理”课上真刀真枪让学生啃下来的最小可行编译器前端(lexer + parser + AST builder + TAC generator),所有模块都刻意保留了调试桩(如print_ast()、dump_ir()),且源码注释密度高到每 3 行就有一行说明逻辑意图。适合两类人:一是刚学完龙书第2–4章、卡在“怎么把理论变成可运行代码”的本科生;二是想快速搭建教学级编译器原型、避免从零写 Flex/Bison 配置的助教或培训讲师。它不解决寄存器分配或目标代码生成,但把“词法→语法→语义→中间表示”这条主链上的每个断点都暴露给你——这才是编译方法课该有的样子。
2. 从解压到跑通:四步构建可验证的编译流程
2.1 解压与目录结构速览:看清哪些文件是“活的”,哪些是“文档”
解压后你会看到如下核心目录结构(已剔除无关日志和临时文件):
SEU-Compiler-Design/ ├── src/ # 主源码目录(Python 实现) │ ├── lexer.py # 基于正则的手工词法分析器(非 Lex) │ ├── parser.py # 递归下降解析器(LL(1) 兼容,无回溯) │ ├── ast.py # AST 节点定义与遍历框架 │ ├── ir_gen.py # 三地址码生成器(含基本块划分) │ └── main.py # 主入口:读取 .c 文件 → 词法分析 → 解析 → IR 生成 → 输出 ├── tests/ # 测试用例集(全部为 .c 后缀,非 .txt) │ ├── test01_simple.c # 单表达式:a = 1 + 2; │ ├── test02_if.c # if-else 分支 │ ├── test03_while.c # while 循环 │ ├── test04_array.c # int a[10]; a[0] = 1; │ └── test05_func.c # void foo() { ... } 调用框架(无参数传递) ├── docs/ # 运行说明(非 PDF,是 README.md + run_guide.txt) │ ├── README.md # 功能概览、依赖、命令示例 │ └── run_guide.txt # 分步调试指令(含 gdb 调试 lexer.py 的提示) ├── Makefile # 支持 make all / make test / make clean └── requirements.txt # 仅依赖 antlr4-python3-runtime==4.9.2(用于可选 AST 可视化)注意:所有
.py文件均采用 Python 3.8+ 语法,无第三方 GUI 或 Web 框架依赖。requirements.txt中的antlr4-python3-runtime仅用于ast_viz.py(可选脚本),不参与核心编译流程——这意味着你即使不装 ANTLR,也能用python src/main.py tests/test01_simple.c直接跑通。
2.2 用最简命令跑通第一个测试:验证环境是否就绪
在项目根目录执行以下命令(无需安装任何编译器工具链,纯 Python 环境即可):
# 步骤1:创建虚拟环境并安装依赖(推荐,避免污染全局) python3 -m venv venv source venv/bin/activate # Linux/macOS # venv\Scripts\activate.bat # Windows # 步骤2:安装唯一运行时依赖(ANTLR 仅用于可视化,此处可跳过) pip install -r requirements.txt # 步骤3:直接运行主程序处理第一个测试用例 python src/main.py tests/test01_simple.c预期输出应为类似以下内容(关键看IR Code:后是否有三地址码):
=== Parsing result === AST root: ProgramNode ├── body: [AssignNode] │ ├── left: IdentifierNode(name='a') │ └── right: BinaryOpNode(op='+', left=IntLiteralNode(value=1), right=IntLiteralNode(value=2)) === IR Code === t0 = 1 t1 = 2 t2 = t0 + t1 a = t2如果看到SyntaxError或NameError,说明环境未就绪;若输出为空或报ModuleNotFoundError: No module named 'antlr4',请忽略——这仅影响后续可视化,不影响 IR 生成。
2.3 用 Makefile 批量验证:5 个测试用例一键回归
Makefile是本项目真正体现工程意识的部分。它不只封装命令,还做了三件事:
- 自动比对期望输出(
tests/expected/下预存各测试的 IR 结果) - 对每个测试生成
.ast.dot文件供 Graphviz 渲染 - 在
make test失败时高亮显示差异行
执行以下命令:
make test你会看到类似输出:
Running test: test01_simple.c ... OK Running test: test02_if.c ... OK Running test: test03_while.c ... OK Running test: test04_array.c ... OK Running test: test05_func.c ... OK All 5 tests passed.逻辑说明:
make test实际调用的是scripts/run_test.sh,该脚本会:
- 对每个
.c文件执行python src/main.py并重定向输出到tmp/xxx.ir;- 用
diff -q对比tmp/xxx.ir与tests/expected/xxx.ir;- 若失败,执行
diff -u tests/expected/xxx.ir tmp/xxx.ir输出上下文差异。
这种设计让你能一眼看出是语法解析错了,还是 IR 生成逻辑有偏差——而不是在 200 行输出里手动找 bug。
2.4 可视化 AST:用 Graphviz 把抽象语法树“画出来”
虽然main.py不直接绘图,但项目提供了ast_viz.py脚本,它利用 ANTLR 的TreeVisitor机制将 AST 导出为 DOT 格式,再调用系统dot命令生成 PNG:
# 确保已安装 Graphviz(Ubuntu: sudo apt install graphviz;macOS: brew install graphviz;Windows: 下载 installer) python scripts/ast_viz.py tests/test02_if.c # 输出:tests/test02_if.ast.png生成的 PNG 文件中,节点按层级展开,IfNode下挂condition、then_body、else_body三个子树,BinaryOpNode明确标出op字段值。这种可视化不是炫技——当你修改parser.py中parse_if_statement()的逻辑时,对比新旧 PNG,能立刻判断是否误删了else分支的递归调用。
参数说明:
ast_viz.py支持两个关键参数:
--no-merge:禁用节点合并(默认会把连续的IntLiteralNode合并显示,开启后每个字面量单独成节点);--max-depth 3:限制渲染深度(防止复杂函数体导致 PNG 过大)。
这些参数在调试深层嵌套语法(如if (a > b) { if (c < d) { ... } })时极为实用。
3. 为什么选递归下降而非 Bison/Yacc?手写解析器的 3 个硬核价值
3.1 教学穿透力:每一行代码都对应龙书中的一个算法步骤
本项目的parser.py是典型的手工递归下降实现,例如parse_expression()函数:
def parse_expression(self): left = self.parse_term() # 对应龙书 Fig 4.12 的 parse_term() while self.current_token.type in [TokenType.PLUS, TokenType.MINUS]: op = self.current_token self.consume_token() # consume '+' or '-' right = self.parse_term() left = BinaryOpNode(op=op.value, left=left, right=right) return left这段代码与《Compilers: Principles, Techniques, and Tools》(龙书)第 4.3.2 节 “Predictive parsers” 中的伪代码几乎一一映射:
self.consume_token()对应书中match(token);self.parse_term()是独立的预测子程序;while循环实现左递归消除后的迭代版本。
而如果你用 Bison 生成 C 代码,看到的将是状态机表驱动的黑匣子——学生能跑通,但无法回答“为什么这里要 shift 而不是 reduce”。本项目强制你直面 FIRST/FOLLOW 集计算、左递归改写、预测分析表构造等核心概念,因为每个if self.current_token.type == ...判断,都是你在纸上推导过的 FIRST 集成员。
3.2 调试友好性:断点打在哪,控制流就停在哪
Bison 生成的解析器一旦报错,堆栈常深达 10 层(yyparse→yy_reduce→yy_action...),而本项目的parser.py断点可精准落在语法错误发生行:
# 在 parse_if_statement() 开头加断点 def parse_if_statement(self): self.consume_token(TokenType.IF) # ← 断点打这里,当输入是 "iff" 时,此处抛出 TokenMismatchError self.consume_token(TokenType.LPAREN) condition = self.parse_expression() self.consume_token(TokenType.RPAREN) ...当测试用例test02_if.c中把if (x > 0)错写成iff (x > 0),程序会在self.consume_token(TokenType.IF)抛出异常,并打印:
TokenMismatchError: Expected IF, got IDENTIFIER('iff') at line 2, column 1这个错误信息包含精确位置(line/column)和期望/实际 token 类型,远比 Bison 默认的syntax error, unexpected IDENTIFIER有用。网安学院要求学生提交 debug log,正是基于这种可追溯性。
3.3 安全扩展接口:为后续注入漏洞检测逻辑留出钩子
网安学院的特殊性在于:他们不只要编译器能跑,还要它能“看穿”潜在风险。本项目在ir_gen.py中预留了visit_ArrayAccessNode方法:
def visit_ArrayAccessNode(self, node): # TODO: 此处可插入边界检查插入逻辑(如生成 if i >= len(a) then panic) base = self.visit(node.array) index = self.visit(node.index) return ArrayAccessNode(base=base, index=index)这个TODO不是占位符,而是明确的教学任务——学生需在此处插入数组越界检查的 IR 生成代码(如t0 = len(a); if i >= t0 goto error_label)。由于整个 IR 生成器是 Visitor 模式,你只需修改单个visit_XXX方法,无需改动 AST 结构或解析逻辑。这种设计让“编译器+安全分析”的融合变得自然,而非后期硬塞。
4. 避坑指南:5 个真实踩过的雷区与血泪解决方案
4.1 现象:make test报diff: command not found,但 Linux 系统明明装了 diff
原因:Makefile中diff命令被硬编码为/usr/bin/diff,而某些最小化 Docker 镜像(如alpine)中diffutils未预装,或 macOS 的diff路径为/usr/bin/diff但功能受限(不支持-u)。
解决:
- 在
Makefile第 3 行添加兼容性判断:DIFF ?= $(shell which diff 2>/dev/null || echo "/usr/bin/diff") # 替换所有 $(DIFF) 调用 - 或直接在宿主机执行
sudo apt install diffutils(Ubuntu)/brew install diffutils(macOS)。
4.2 现象:python src/main.py tests/test04_array.c输出 IR 中数组下标恒为0,无论源码写a[5]还是a[i]
原因:lexer.py中对数字字面量的正则匹配写成了r'[0-9]+',但未处理负数(如-1)和十六进制(如0xFF),更致命的是——它把标识符i也误判为数字字面量(因正则未锚定边界)。
解决:
修改lexer.py的token_patterns列表,将数字匹配改为:
(r'\b[0-9]+\b', TokenType.INT_LITERAL), # \b 确保单词边界 (r'\b0[xX][0-9a-fA-F]+\b', TokenType.HEX_LITERAL), (r'-[0-9]+', TokenType.NEG_INT_LITERAL), # 单独处理负号并确保IDENTIFIER规则(r'[a-zA-Z_][a-zA-Z0-9_]*')排在数字规则之后——否则i123会被截成i+123。
4.3 现象:ast_viz.py生成的 PNG 中中文注释显示为方框,且节点重叠严重
原因:Graphviz 默认字体不支持 UTF-8,且未设置rankdir=TB(从上到下布局),导致长标识符节点挤压子树。
解决:
在ast_viz.py的generate_dot()函数末尾,添加字体与布局配置:
dot_content += 'graph [fontname="SimHei", fontsize=12];\n' # 中文字体 dot_content += 'node [fontname="SimHei", fontsize=10];\n' dot_content += 'edge [fontname="SimHei", fontsize=9];\n' dot_content += 'rankdir=TB;\n' # 强制纵向布局并在系统中安装simhei.ttf(Linux 可复制到/usr/share/fonts/后执行fc-cache -fv)。
4.4 现象:test05_func.c中void foo() { int x = 1; }解析时报Unexpected token '}'
原因:parser.py的parse_function_definition()方法中,parse_block()未正确处理空语句块(即{}),导致}被当作parse_statement()的输入,而parse_statement()未定义对RBRACE的处理分支。
解决:
在parse_statement()开头添加兜底逻辑:
def parse_statement(self): if self.current_token.type == TokenType.RBRACE: return EmptyStatementNode() # 新增空语句节点 # 后续原有逻辑...并在ast.py中定义EmptyStatementNode类,其accept()方法直接返回None。
4.5 现象:在 Windows 上执行make报错'make' is not recognized as an internal or external command
原因:Windows 默认无make,而项目未提供make.bat或 PowerShell 替代方案。
解决:
- 方案一(推荐):安装
mingw-w64(含mingw32-make),将其bin目录加入 PATH,然后用mingw32-make替代make; - 方案二(免安装):直接执行
scripts/run_test.sh中的等价命令(需用 Git Bash 或 WSL):for f in tests/*.c; do python src/main.py "$f" > "tmp/$(basename "$f" .c).ir"; done
5. 进阶技巧:把课程设计变成可交付的轻量级 DSL 编译器
5.1 修改文法:从 C 子集到自定义 DSL 的三处关键剪裁
本项目原始文法(见docs/grammar.md)本质是简化 C:支持int/void、if/while、+/-/*//、数组访问。若你想将其转为领域专用语言(DSL),比如“网络策略描述语言”,只需三处修改:
| 修改点 | 原 C 文法元素 | DSL 替代方案 | 修改文件 |
|---|---|---|---|
| 数据类型 | int,void | policy,rule,action | lexer.py(新增POLICY/RULEtoken)、parser.py(parse_type_specifier()返回 PolicyTypeNode) |
| 控制结构 | if (cond) { ... } | when src_ip == "192.168.1.0/24" then allow | parser.py(重写parse_if_statement()为parse_when_clause(),consume_token(TokenType.WHEN)) |
| 操作符 | +,-,== | ==,!=,in,matches | lexer.py(新增IN,MATCHEStoken)、ir_gen.py(visit_BinaryOpNode中为in生成t0 = contains(src_ip, "192.168.1.0/24")) |
实操建议:先用
git checkout -b dsl-policy新建分支,然后按上表顺序修改。每次修改后,用make test验证存量测试是否仍通过——这能确保你的 DSL 扩展不破坏原有架构。
5.2 IR 生成器升级:从三地址码到 JSON Schema 可验证输出
当前ir_gen.py输出文本格式 TAC,但若要对接下游策略引擎,JSON 更合适。新增json_ir_gen.py:
class JSONIRGenerator(ASTVisitor): def __init__(self): self.ir = {"statements": []} def visit_AssignNode(self, node): self.ir["statements"].append({ "type": "assign", "target": node.left.name, "value": self._expr_to_json(node.right) }) def _expr_to_json(self, expr): if isinstance(expr, BinaryOpNode): return { "op": expr.op, "left": self._expr_to_json(expr.left), "right": self._expr_to_json(expr.right) } elif isinstance(expr, IntLiteralNode): return {"type": "int", "value": expr.value} # ... 其他节点类型然后在main.py中添加--output-format json参数,调用JSONIRGenerator().visit(ast_root)。这样输出就是标准 JSON,可直接被jsonschema验证,或喂给 Go 写的策略执行器。
5.3 构建 CI/CD 流水线:用 GitHub Actions 实现每次 push 自动验证
在项目根目录添加.github/workflows/test.yml:
name: Compile & Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v4 with: python-version: '3.9' - name: Install dependencies run: pip install -r requirements.txt - name: Run tests run: make test - name: Generate AST diagrams (optional) if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | pip install graphviz python scripts/ast_viz.py tests/test01_simple.c git config --local user.name 'github-actions' git config --local user.email 'actions@github.com' git add tests/test01_simple.ast.png git commit -m "Update AST diagram" || echo "No changes to commit" git push这个 workflow 每次 push 都会:
- 在 Ubuntu 环境中复现学生本地环境;
- 运行
make test并将结果作为 Checks 显示在 PR 页面; - (可选)自动更新
test01_simple.ast.png,让文档始终与代码同步。
我的习惯:我会在
tests/目录下放一个regression.log,每次make test成功后追加一行$(date): PASS。当某天make test突然失败,翻这个 log 就能定位是哪次提交引入的 regression——这比看 CI 日志快得多。希望帮到你。
本文还有配套的精品资源,点击获取