尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

西北工业大学编译原理大作业:Sysy编译器完整实现与避坑指南

西北工业大学编译原理大作业:Sysy编译器完整实现与避坑指南 简介这份资源是西北工业大学编译原理试点班的大作业完整交付物面向计算机、人工智能、通信工程等专业正在学习编译原理或准备课程设计的学生也适合需要参考完整编译器实现路径的自学者。包内共168个文件以111个sy测试用例、22个in输入数据、8个cpp源文件与7个h头文件为核心辅以少量c、py、ll、md及Makefile等构建与说明文件压缩包约113KB体量轻便却覆盖了从词法、语法到中间代码生成与优化的关键环节。内容预览可见sylib.c、ast.cpp、passes.cpp、IR.cpp、SSABuilder.cpp等模块说明项目包含AST构建、遍管理与SSA构造等典型编译器组件。已有249人学习下载答辩评审平均分达96分代码均经测试运行成功。下载后可按README说明快速上手既能作为课程设计或毕设的参考实现也便于在此基础上修改扩展用于作业提交或项目初期立项演示。1. 从 Sysy 到可执行一份能跑通的编译原理大作业长什么样如果你正在搜“西北工业大学编译原理试点班大作业”大概率不是想听编译原理导论而是想找一份能编译、能跑、能答辩的完整实现。Sysy 是很多高校编译课用的类 C 教学语言语法比 C 精简但五脏俱全变量声明、函数、数组、条件、循环、表达式优先级一个不少。这份资源给的是一个完整编译器从词法、语法、AST、语义检查、IR、SSA 到目标代码生成都有对应文件还带文档说明和实验报告。它适合三类人课设卡在某个 pass 写不动的、想对照一份成熟结构补自己缺环的、以及需要一份能改能扩的基线做毕设或立项演示的。下面我按“先看结构、再跑起来、最后避坑”的顺序拆一遍。2. 源码结构拆解从 sylib.c 到 SSABuilder.cpp 的职责边界拿到一个编译器源码包最忌讳上来就make。先花十分钟把文件职责理清后面调 bug 能省几个小时。这份资源的文件命名很直白基本按编译阶段切分下面按我实际阅读的顺序讲。2.1 运行时库与前端入口sylib.c、preDeclareFunc.c、hello.csylib.c是 Sysy 的运行时支持库通常包含getint、putint、getch、putch这类内置函数的实现编译器生成的目标代码会链接它。preDeclareFunc.c是预声明函数表把运行时函数提前注册进符号表避免语义分析阶段报“未定义函数”。hello.c是最小测试用例一般就是int main(){ putint(1); return 0; }这种用来验证工具链是否打通。这三个文件的关系是hello.c是输入preDeclareFunc.c保证输入里调用的内置函数能被识别sylib.c保证最终链接出的可执行文件真的能打印。很多人编译通过但运行报undefined reference to getint就是sylib.c没参与链接。常见做法是写一个Makefile或CMakeLists.txt把sylib.c编成静态库或对象文件最后和编译器产出的汇编/目标文件一起链接。我一般会先单独编译sylib.c确认没有语法问题# 先确认运行时库能独立编译排除环境问题 gcc -c sylib.c -o sylib.o # 如果这一步就报错说明缺头文件或编译器版本不匹配参数说明-c只编译不链接-o指定输出对象文件名。这一步过了说明基础工具链没问题再往下查编译器本身。2.2 AST 与语义ast.cpp、param_list.cppast.cpp是抽象语法树的节点定义和构造逻辑通常包含表达式节点、语句节点、函数定义节点的类层次。param_list.cpp专门处理函数参数列表因为 Sysy 支持多参数和数组参数参数列表的解析和类型检查单独抽出来更清晰。读ast.cpp时重点看两件事一是节点类型是否覆盖了 Sysy 全部语法if、while、break、continue、数组、函数调用二是每个节点有没有挂type字段语义分析阶段要靠它做类型推导。param_list.cpp则要看它怎么处理int a[]这种数组参数——是退化成指针还是保留维度信息这直接影响后面 IR 生成。我一般会拿一个覆盖全语法的测试用例反推 AST 是否完整// test_full.c 覆盖声明、数组、函数、控制流 int g[10]; int fib(int n) { if (n 1) return n; return fib(n-1) fib(n-2); } int main() { int i 0; while (i 10) { g[i] fib(i); i i 1; } return g[9]; }如果这份源码能把这个用例解析成 AST 并打印出来说明前端基本完整。打印 AST 的调试代码一般藏在main.cpp的某个-dump-ast分支里没有的话自己加几行std::cout也不难。2.3 中端与后端passes.cpp、IR.cpp、SSABuilder.cppIR.cpp定义中间表示通常是三地址码或四元式。SSABuilder.cpp负责把普通 IR 转成 SSA静态单赋值形式每个变量只赋值一次方便后续优化。passes.cpp是优化 pass 的集合常见的有常量折叠、死代码消除、公共子表达式消除。这三者的依赖关系是IR.cpp提供数据结构SSABuilder.cpp做形式转换passes.cpp在 SSA 上跑优化。如果SSABuilder.cpp的 phi 节点插入位置不对后面所有优化都会出错而且报错往往很隐蔽——生成的目标代码能跑但结果不对。我建议的验证顺序是先关掉所有优化 pass确认IR.cpp生成的朴素 IR 能正确翻译成目标代码再单独打开SSABuilder用同一个测试用例对比输出最后逐个打开 pass每开一个跑一遍回归。这样出问题能立刻定位到是哪个阶段引入的。# 假设编译器可执行文件叫 sysyc ./sysyc -O0 test_full.c -o test_O0.s # 无优化 ./sysyc -O1 test_full.c -o test_O1.s # 开 SSA 基础优化 diff (gcc test_O0.s sylib.o -o a0 ./a0; echo $?) \ (gcc test_O1.s sylib.o -o a1 ./a1; echo $?)参数说明-O0和-O1是常见优化级别开关具体名字以源码main.cpp里的参数解析为准。diff那行是比对两个版本的可执行文件返回值返回值一致说明优化没有改变语义。2.4 驱动与测试main.cpp、test.cppmain.cpp是编译器入口负责解析命令行参数、按顺序调用各阶段、输出结果。test.cpp通常是单元测试或集成测试的集合可能包含一批.c用例和预期输出。读main.cpp时重点看参数解析部分确认支持哪些开关-o、-S、-dump-ast、-O1等这决定了你怎么调试。test.cpp则要看它怎么组织用例——是硬编码字符串还是读文件前者改起来快后者更适合批量回归。我一般会先跑test.cpp里的用例确认基线通过率再动任何代码。如果test.cpp依赖某个测试框架比如 gtest先确认环境里装了没有没装就手动把用例抽出来跑。3. 把编译器跑起来环境、编译、链接与第一个可执行文件结构理清后下一步是让它真的跑起来。这一章按“装依赖 → 编编译器 → 编测试用例 → 链接运行”的顺序走每步都给可抄的命令。3.1 环境准备gcc、g、make、flex/bison 的版本要求这份源码是 C/C 混合前端可能用 flex/bison 生成词法语法分析器也可能手写递归下降。先看目录里有没有.l和.y文件有就装 flex/bison没有就只需要 g 和 make。# Ubuntu/Debian 环境 sudo apt update sudo apt install -y build-essential flex bison cmake # 确认版本g 建议 9 以上C17 特性用得多 g --version flex --version bison --version参数说明build-essential包含 gcc、g、makeflex和bison只在有.l/.y文件时需要。如果源码用了 CMake还要装cmake。我遇到过 g 版本太低导致std::optional编译失败的情况升级到 9 以上就解决了。提示如果是在 Windows 上建议用 WSL2 或 MSYS2纯 Visual Studio 编译这份源码大概率会缺 POSIX 头文件。3.2 编译编译器本体make 与 CMake 两条路径先看根目录有没有CMakeLists.txt。有就走 CMake没有就看Makefile。# 路径 A有 CMakeLists.txt mkdir -p build cd build cmake .. -DCMAKE_BUILD_TYPERelease make -j$(nproc) # 路径 B只有 Makefile make -j$(nproc)参数说明-DCMAKE_BUILD_TYPERelease开优化编译出的编译器跑得快-j$(nproc)用满 CPU 核数并行编译。如果编译报错先看第一个 error后面的往往是连锁反应。常见错误是缺头文件比如#include cstdint没写补上即可。编译成功后在build/或根目录下应该有一个可执行文件名字可能是sysyc、compiler或main。用ls -lt按时间排序找最新生成的可执行文件。3.3 编译并运行第一个 Sysy 程序假设编译器叫sysyc测试用例是hello.c# 第一步把 Sysy 源码编译成汇编 ./sysyc -S hello.c -o hello.s # 第二步汇编成目标文件 gcc -c hello.s -o hello.o # 第三步链接运行时库 gcc hello.o sylib.o -o hello # 第四步运行 ./hello参数说明-S表示只生成汇编不汇编第二步用gcc -c而不是as因为gcc会自动处理平台相关的汇编伪指令第三步必须带上sylib.o否则putint这类函数找不到定义。如果./sysyc报“unknown option -S”说明参数名不一样去main.cpp里搜argv看实际支持哪些开关。有的实现用-o直接输出可执行文件那就省掉第二三步。3.4 用 test.cpp 做回归确认基线通过率跑通 hello 之后别急着改代码。先跑test.cpp里的用例记录通过率。# 如果 test.cpp 是独立可执行文件 g test.cpp -o test_runner -I. -L. -lsysy ./test_runner # 如果是脚本驱动的批量测试 bash run_tests.sh 21 | tee baseline.log参数说明-I.把当前目录加入头文件搜索路径-L.加入库搜索路径-lsysy链接名为libsysy.a或libsysy.so的库。tee baseline.log把输出同时打到屏幕和文件方便后面改代码后对比。我一般会把基线通过率记在 README 里比如“50 个用例通过 47 个3 个失败是数组越界和嵌套函数”。这样后面每改一个 pass跑一遍就知道有没有引入回归。4. 避坑与排查编译原理大作业最容易翻车的五个点这一章是我自己踩过和帮别人调过的坑按“现象 → 原因 → 解决”写。每个都真实发生过不是理论推演。4.1 现象编译通过但运行段错误gdb 栈里全是 phi 节点原因SSABuilder.cpp插入 phi 节点时没有正确处理循环头的前驱块导致某个变量在循环入口处引用了未定义的 SSA 值。这种错误在 IR 层面看不出来只有生成目标代码后运行才崩。解决在SSABuilder里加一个校验每个 phi 节点的操作数数量必须等于前驱块数量。不相等就打印块名和变量名定位到具体循环。我一般会在passes.cpp里加一个-verify-ssa开关跑测试时默认打开。4.2 现象数组访问结果错位g[1] 读出来是 g[0] 的值原因ast.cpp里数组下标表达式的类型推导写成了int而不是int*导致 IR 生成时把数组当标量处理地址计算少乘了元素大小。解决检查ast.cpp中ArrayRef节点的type字段确保它是int*或带维度信息的数组类型。然后在IR.cpp的地址计算里确认有index * sizeof(int)这一步。用g[1] 42; putint(g[1]);这种最小用例验证。4.3 现象函数调用参数超过 6 个时结果错乱原因目标代码生成阶段没有正确处理栈传递参数。x86-64 下前 6 个整型参数走寄存器第 7 个开始走栈如果param_list.cpp和调用约定没对齐就会读到错误的值。解决在param_list.cpp里明确区分寄存器参数和栈参数生成调用代码时按 ABI 顺序压栈。测试用例写一个 8 参数的函数每个参数打印出来确认顺序和值都对。4.4 现象make 报 “undefined reference to yylex”原因flex 生成的lex.yy.c没有加入编译目标或者Makefile里漏了-lfl。解决确认Makefile的SRCS变量包含lex.yy.c和y.tab.c链接时加-lfl。如果用手写词法分析器检查main.cpp里有没有调用yylex()的声明。4.5 现象优化打开后程序输出正确但死循环原因passes.cpp里的死代码消除误删了循环条件更新语句因为 SSA 形式下循环变量的 phi 节点被判定为“无副作用”而删除。解决在死代码消除 pass 里加白名单phi 节点和分支条件相关的指令不参与删除。更稳妥的做法是每个 pass 跑完都做一次 CFG 校验确认没有不可达块和悬空跳转。注意优化 pass 的 bug 最难查因为-O0正确不代表-O1正确。我习惯每加一个 pass 就跑一遍全量回归通过率不掉才继续。5. 进阶用法在现有编译器上加一个常量传播 pass跑通基线后如果想拿这份代码做毕设或课设加分最实际的扩展是加一个常量传播constant propagationpass。它比死代码消除简单效果又明显答辩时好讲。5.1 常量传播的原理与在 SSA 上的优势常量传播的目标是如果某个变量在编译期能确定值就把所有使用它的地方替换成常量。在 SSA 形式下这件事特别好做因为每个变量只赋值一次只要沿着 def-use 链走遇到phi节点时取所有操作数的交集即可。比如int x 3; int y x 4; return y;在 SSA 下x只有一个定义x1 3y1 x1 4可以直接折叠成y1 7return y1变成return 7。如果x在分支里被赋不同值phi节点的操作数不全是常量就放弃传播。5.2 在 passes.cpp 里挂载新 pass 的步骤假设passes.cpp里已经有一个 pass 注册表加新 pass 分三步// 第一步定义 pass 类继承基类 class ConstPropPass : public Pass { public: void run(Function F) override { for (auto BB : F) { for (auto I : BB) { // 只处理二元运算指令 if (auto *bin dyn_castBinaryInst(I)) { if (bin-lhs()-isConst() bin-rhs()-isConst()) { int val bin-evalConst(); bin-replaceAllUsesWith(new ConstInst(val)); } } } } } }; // 第二步在 pass 管理器里注册 void registerPasses(PassManager PM) { PM.addPass(new ConstPropPass()); // 其他 pass ... } // 第三步在 main.cpp 的参数解析里加开关 if (opt -const-prop) { PM.enable(const-prop); }逻辑说明dyn_cast是 LLVM 风格的类型转换失败返回空指针isConst()判断操作数是否是常量replaceAllUsesWith把所有使用旧指令的地方替换成新常量。参数说明-const-prop是自定义开关加在main.cpp的argv循环里和-O1并列。5.3 验证常量传播是否生效对比 IR 与运行结果加完 pass 后用同一个测试用例对比优化前后的 IR./sysyc -emit-ir test_const.c -o before.ir ./sysyc -emit-ir -const-prop test_const.c -o after.ir diff before.ir after.ir如果after.ir里add指令变成了常量说明生效。再跑一遍可执行文件确认返回值不变./sysyc -const-prop test_const.c -o test_const.s gcc test_const.s sylib.o -o test_const ./test_const; echo $?返回值应该和优化前一致。如果不一致大概率是phi节点的常量合并逻辑写错了回去检查ConstPropPass里对phi的处理。5.4 一个具体技巧用 -emit-ir 做 pass 级调试很多编译器实现只支持-S输出汇编不支持输出 IR。我强烈建议在main.cpp里加一个-emit-ir开关把每个 pass 跑完后的 IR 打印出来。这样调优化 bug 时不用反复编译链接直接看 IR 就能定位。// 在 pass 管理器每次 run 之后打印 IR if (emitIR) { F.print(llvm::outs()); llvm::outs() \n; }参数说明emitIR是全局标志由-emit-ir设置F.print是 IR 模块的打印方法具体名字以IR.cpp里的实现为准。加了这个开关后我调常量传播只花了半小时之前没加的时候靠 gdb 看汇编花了整整一晚上。从那以后我每次动优化 pass都强制先跑-emit-ir对比前后差异再跑可执行文件确认语义。这个习惯帮我省了至少三次通宵。希望帮到你。本文还有配套的精品资源点击获取
返回列表