可调试可扩展的编译器前端实践:从C子集到DSL
简介本资源是东南大学网络安全学院《编译方法》课程设计实践包面向计算机/网安专业本科生及编译原理初学者聚焦词法分析、语法解析、语义处理与代码生成等核心环节的动手实现。压缩包含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-runtime4.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(namea) │ └── right: BinaryOpNode(op, leftIntLiteralNode(value1), rightIntLiteralNode(value2)) 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# 确保已安装 GraphvizUbuntu: sudo apt install graphvizmacOS: brew install graphvizWindows: 下载 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(opop.value, leftleft, rightright) 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(basebase, indexindex)这个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 diffutilsUbuntu/brew install diffutilsmacOS。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会被截成i123。4.3 现象ast_viz.py生成的 PNG 中中文注释显示为方框且节点重叠严重原因Graphviz 默认字体不支持 UTF-8且未设置rankdirTB从上到下布局导致长标识符节点挤压子树。解决在ast_viz.py的generate_dot()函数末尾添加字体与布局配置dot_content graph [fontnameSimHei, fontsize12];\n # 中文字体 dot_content node [fontnameSimHei, fontsize10];\n dot_content edge [fontnameSimHei, fontsize9];\n dot_content rankdirTB;\n # 强制纵向布局并在系统中安装simhei.ttfLinux 可复制到/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 或 WSLfor f in tests/*.c; do python src/main.py $f tmp/$(basename $f .c).ir; done5. 进阶技巧把课程设计变成可交付的轻量级 DSL 编译器5.1 修改文法从 C 子集到自定义 DSL 的三处关键剪裁本项目原始文法见docs/grammar.md本质是简化 C支持int/void、if/while、/-/*//、数组访问。若你想将其转为领域专用语言DSL比如“网络策略描述语言”只需三处修改修改点原 C 文法元素DSL 替代方案修改文件数据类型int,voidpolicy,rule,actionlexer.py新增POLICY/RULEtoken、parser.pyparse_type_specifier()返回 PolicyTypeNode控制结构if (cond) { ... }when src_ip 192.168.1.0/24 then allowparser.py重写parse_if_statement()为parse_when_clause()consume_token(TokenType.WHEN)操作符,-,,!,in,matcheslexer.py新增IN,MATCHEStoken、ir_gen.pyvisit_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.pyclass 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.ymlname: Compile Test on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 - name: Set up Python uses: actions/setup-pythonv4 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 actionsgithub.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 日志快得多。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →