简介这份资源是四川大学编译原理课程的实验课配套资料面向正在学习编译原理、需要动手实现编译器各阶段组件的高校学生。内容按周次组织覆盖词法分析、语法分析、语义分析与代码生成等核心环节并配有tiny与c-两套语言的测试用例便于对照验证。压缩包共53个文件、约2.24MB以txt测试用例、h头文件、c与c-源码、tny样例、makefile构建脚本为主另有docx说明文档、pptx课程设计讲稿和README.md源码与文档相互配套方便按实验阶段逐步推进。目前已有158人学习。读者可借助各周实验的源代码工程与说明文档理解词法扫描、递归下降或LR分析、类型检查与三地址码生成等具体实现思路并通过测试用例排查错误、巩固编译器设计流程为课程作业与后续语言工具开发积累可复用的工程经验。1. 从一份川大编译原理实验包说起谁需要它能省下多少事如果你正在上编译原理这门课或者带这门课的助教大概率经历过这样的场景Week 6 的词法分析刚写完Week 8 的语法分析就来了老师只给了一份 PDF 说明和几个测试用例剩下的全靠自己啃。这份四川大学编译原理课程的实验课代码与报告压缩包就是针对这个痛点整理的资源集合。它按周次划分覆盖词法扫描、语法分析、语义分析到代码生成四个阶段每个阶段都包含源代码工程、makefile、测试用例和说明文档。适合三类人正在跟实验进度的本科生、需要参考实现思路的助教、以及想用 C 语言手写一遍编译器前端来巩固基础的从业者。它不是一份“看完就能懂”的教程而是一套可以编译、可以跑测试用例、可以对照报告理解设计决策的工程材料。2. 拆开压缩包四个实验阶段的文件结构与技术选型2.1 按周次划分的目录布局与对应编译阶段拿到压缩包后第一件事不是急着编译而是先看清楚目录结构。从文件清单来看资源按 Week 6、Week 8、Week 10、Week 12 四个时间节点组织每个节点下都有独立的源代码工程、makefile、说明文档和测试用例。Week 6 对应词法扫描目录里有c1-sample.txt、t1.tny、t1-sample.txt这些测试输入以及dfa.gv这个用 Graphviz 描述的有穷自动机图文件。Week 8 进入语法分析测试用例变成c1.txt、c2.txt和对应的c1-sample.txt、c2-sample.txt源代码工程里出现了c1.c、c2.c、main.c和types.h。Week 10 和 Week 12 分别对应语义分析和代码生成目录里保留了说明文档.docx和源代码工程文件夹但测试用例的命名规则延续了前面的风格。这种按周次切分的方式有一个好处每个阶段的代码是独立可编译的不会因为后面阶段的修改导致前面阶段的测试跑不通。常见做法是每个 Week 目录下都有一个完整的 makefile直接make就能生成可执行文件。我一般会先把每个 Week 的说明文档过一遍确认输入输出格式再去看源代码。2.2 词法扫描与语法分析的核心数据结构Week 6 的词法扫描器核心是一个 DFA确定有穷自动机。dfa.gv文件用 DOT 语言描述了状态转移关系可以用 Graphviz 渲染成图来看。源代码工程里的main.c负责读取输入文件逐字符驱动状态机输出 token 流。types.h里定义了 token 类型枚举和符号表结构。Week 8 的语法分析器在词法分析的基础上构建抽象语法树c1.c和c2.c分别对应两种不同的语法结构测试用例main.c里通常包含递归下降分析的入口函数。这里有一个选型上的细节为什么用 C 而不是 C 或 Python从文件扩展名.c和.h来看整个工程是纯 C 风格。纯 C 写编译器前端的好处是内存布局可控符号表和语法树节点可以用结构体加指针的方式管理适合理解底层机制。代价是手动内存管理容易出问题尤其是语法分析阶段递归下降时如果 AST 节点释放不干净跑大测试用例会内存泄漏。2.3 测试用例的命名规则与输入格式测试用例的命名有规律可循。t1.tny、t2.tny是 tiny 语言的源程序t1-sample.txt、t2-sample.txt是对应的期望输出。c1.txt、c2.txt是 C 语言子集的测试输入c1-sample.txt、c2-sample.txt是期望的 token 流或语法树输出。Week 6 的c1-sample.txt和 Week 8 的c1-sample.txt内容不同前者是词法单元序列后者是语法分析结果。跑测试的时候常见做法是用diff对比实际输出和 sample 文件。如果输出格式有细微差异比如多了一个空格或少了一个换行diff 就会报错。我一般会先用cat -A看 sample 文件的行尾和制表符确认格式要求。3. 从 makefile 到可执行文件编译、运行与测试用例验证3.1 编译环境的准备与 makefile 参数解读在编译之前需要确认环境里有gcc、make和flex如果词法分析器用了 flex 生成。从文件清单看Week 6 的源代码工程里没有.l文件说明词法扫描器是手写的 DFA不依赖 flex。Week 8 也没有.y文件语法分析器是递归下降手写的。这意味着编译依赖很少只需要标准 C 库。进入任意一个 Week 的源代码工程目录先看 makefile 内容。典型的 makefile 会定义CC gcc、CFLAGS -Wall -g、TARGET parser这些变量然后通过$(TARGET): $(OBJS)规则链接。-g选项保留调试符号方便用 gdb 排查段错误。-Wall打开所有警告手写编译器前端时未初始化的变量和类型不匹配是常见问题警告能提前暴露。# 进入 Week 8 语法分析工程目录 cd Week 8 词法扫描测试用例/源代码工程 # 查看 makefile 内容确认编译目标和依赖 cat makefile # 编译生成可执行文件 make # 如果编译报错先清理再重试 make clean make这段命令的逻辑是先进入 Week 8 的源代码工程目录查看 makefile 确认编译规则然后执行make。如果报错make clean会删除之前生成的.o文件和可执行文件避免旧对象文件干扰。参数说明cd后面的路径需要根据实际解压位置调整如果路径里有空格必须用引号包起来。3.2 运行词法扫描器并对比 sample 输出编译成功后用测试用例验证。以 Week 6 的词法扫描器为例假设生成的可执行文件叫scanner输入文件是t1.tny期望输出是t1-sample.txt。# 运行词法扫描器将输出重定向到临时文件 ./scanner t1.tny my_output.txt # 对比实际输出和期望输出 diff my_output.txt t1-sample.txt # 如果 diff 没有输出说明完全一致 # 如果有差异用 cat -A 查看不可见字符 cat -A my_output.txt | head -20 cat -A t1-sample.txt | head -20逻辑说明./scanner t1.tny调用扫描器读取源文件 my_output.txt把标准输出保存到文件。diff逐行对比两个文件没有输出表示一致。如果有差异cat -A会显示行尾的$和制表符^I帮助定位是空格还是换行的问题。参数说明head -20只显示前 20 行避免输出过长。3.3 语法分析器的输入输出与 AST 打印格式Week 8 的语法分析器通常接受词法单元流或源文件作为输入输出抽象语法树的文本表示。从c1-sample.txt和c2-sample.txt的内容来看AST 的打印格式可能是缩进括号表示法比如(program (stmt (assign x (expr 1))))这种形式。# 运行语法分析器输入 c1.txt输出 AST ./parser c1.txt ast_output.txt # 对比期望的 AST 输出 diff ast_output.txt c1-sample.txt # 如果输出格式不对检查 parser 里的打印函数 # 常见问题是缩进层级或括号匹配错误逻辑说明语法分析器的输入是c1.txt输出是 AST 的文本表示。diff对比期望输出。如果格式不一致需要检查main.c或c1.c里的 AST 打印函数确认缩进和括号是否与 sample 文件一致。参数说明c1.txt是测试输入c1-sample.txt是期望输出两者文件名相似但内容不同不要搞混。3.4 用 makefile 批量跑测试用例手动一个个跑测试用例效率低常见做法是在 makefile 里加一个test目标自动遍历所有输入文件并对比输出。# 在 makefile 末尾添加 test 目标 test: $(TARGET) for input in t1.tny t2.tny; do \ expected$${input%.tny}-sample.txt; \ ./$(TARGET) $$input /tmp/output.txt; \ if diff -q /tmp/output.txt $$expected /dev/null; then \ echo PASS: $$input; \ else \ echo FAIL: $$input; \ diff /tmp/output.txt $$expected; \ fi; \ done逻辑说明test目标依赖$(TARGET)确保可执行文件已编译。for循环遍历所有.tny文件$${input%.tny}去掉扩展名拼出-sample.txt文件名。diff -q静默对比一致则输出 PASS不一致则输出 FAIL 并显示差异。参数说明/tmp/output.txt是临时文件避免污染工程目录。符号抑制 make 回显命令本身。4. 避坑与排查手写编译器前端时最容易翻车的五个地方4.1 现象编译通过但运行时段错误原因符号表指针未初始化解决用 gdb 定位空指针手写词法扫描器时符号表通常用链表或哈希表实现。如果types.h里定义了struct symbol *sym_table但main.c里忘记初始化第一次插入 token 时就会解引用空指针。现象是编译没问题一跑测试用例就 segfault。解决方法是编译时加-g然后用gdb ./scanner运行run t1.tny后bt看调用栈定位到具体行。常见做法是在main函数开头显式初始化所有全局指针。4.2 现象diff 显示输出多了一行空行原因printf 里多了\n解决用cat -A对比行尾词法扫描器输出 token 流时每个 token 占一行。如果printf(%s\n, token_name)写成了printf(%s\n\n, token_name)输出就会多一个空行。diff会报空行。用cat -A my_output.txt看到行尾是$还是$加空行就能确认。解决方法是检查所有printf语句确保换行符数量与 sample 文件一致。4.3 现象语法分析器递归下降时栈溢出原因左递归未消除解决改写文法或改用迭代递归下降分析器要求文法不含左递归。如果文法里有expr - expr term这样的规则直接写成递归函数会导致无限递归最终栈溢出。现象是程序跑几秒后崩溃gdb 显示调用栈有几千层parse_expr。解决方法是消除左递归改写成expr - term expr_tailexpr_tail - term expr_tail | ε。或者用迭代加栈的方式手动模拟递归。4.4 现象make 报错undefined reference to yylex原因手写扫描器函数名不匹配解决统一函数命名如果语法分析器里调用了yylex()但词法扫描器里定义的函数叫get_token()链接时就会报undefined reference。现象是编译.c文件没问题链接阶段失败。解决方法是检查main.h里的函数声明和main.c里的定义是否一致。常见做法是把词法扫描器的入口函数统一命名为yylex即使不用 flex也保持这个命名习惯。4.5 现象测试用例通过但报告里描述的设计与代码不符原因报告和代码版本不同步解决先跑通代码再写报告Week 10 和 Week 12 的说明文档里描述了语义分析和代码生成的实现思路但源代码工程可能只包含部分实现。如果直接照搬报告里的描述去理解代码会发现对不上。解决方法是先编译运行代码确认实际功能再对照报告看哪些是设计目标、哪些是已实现部分。常见做法是报告里写“计划采用三地址码”但代码里只生成了抽象语法树这种差异要在报告里说明。5. 进阶用法用 dfa.gv 可视化自动机与扩展 tiny 语言5.1 用 Graphviz 渲染 dfa.gv 并验证状态转移Week 6 的dfa.gv文件是理解词法扫描器设计的关键。它用 DOT 语言描述了 DFA 的状态和转移边。安装 Graphviz 后可以用dot命令把它渲染成 PNG 或 SVG。# 安装 GraphvizUbuntu/Debian sudo apt-get install graphviz # 渲染 dfa.gv 为 PNG 图片 dot -Tpng dfa.gv -o dfa.png # 如果 dfa.gv 里有中文标签指定字体 dot -Tpng -GfontnameSimHei dfa.gv -o dfa.png逻辑说明dot -Tpng指定输出格式为 PNG-o dfa.png指定输出文件名。如果渲染报错检查dfa.gv里的语法比如节点名是否用了非法字符。参数说明-Gfontname设置默认字体避免中文乱码。渲染出来的图可以对照main.c里的switch语句确认每个状态转移是否与代码一致。5.2 扩展 tiny 语言的词法规则并重新生成扫描器tiny 语言的词法规则比较简单只支持基本的关键字、标识符、数字和运算符。如果想扩展比如加入和-复合赋值运算符需要修改dfa.gv和main.c里的状态机。// 在 main.c 的状态机里增加对 后跟 的处理 case STATE_PLUS: if (ch ) { token.type TOKEN_PLUS_ASSIGN; token.value ; state STATE_START; } else { token.type TOKEN_PLUS; token.value ; // 回退一个字符因为 已经消费了 ungetc(ch, fp); state STATE_START; } break;逻辑说明当状态机处于STATE_PLUS时读取下一个字符。如果是生成token否则生成token并用ungetc把多读的字符放回输入流。参数说明ungetc是 C 标准库函数用于回退一个字符但只能回退一个且不能保证一定成功。常见做法是用缓冲区加指针的方式管理输入避免依赖ungetc。5.3 用 sample 文件反向验证代码生成的正确性Week 12 的代码生成阶段测试用例的 sample 文件可能是目标代码的文本表示比如三地址码或虚拟机指令。验证方法是先跑通代码生成器得到输出再逐行对比 sample 文件。检查项方法常见问题指令顺序diff 对比表达式求值顺序错误临时变量命名正则匹配临时变量重复或跳号标签跳转检查跳转目标标签未定义或跳转错误寄存器分配对比 sample寄存器冲突或溢出如果 sample 文件里的临时变量是t1, t2, t3这种顺序而实际输出是t1, t3, t2说明表达式求值顺序有问题。常见做法是在 AST 遍历时先遍历左子树再遍历右子树确保临时变量按顺序生成。5.4 把实验代码移植到其他语言子集时的注意事项这套实验代码针对的是 tiny 语言和 C 语言子集。如果想移植到其他语言比如 Python 子集或 JavaScript 子集需要注意几点第一词法规则不同关键字和运算符集合要重新定义第二语法结构不同递归下降的文法要重写第三语义分析的符号表管理方式可能不同比如 Python 的作用域规则比 C 复杂。我一般会先保留词法扫描器的框架只替换 token 定义和 DFA 转移表然后重写语法分析器。代码生成阶段可以复用三地址码的生成逻辑只调整目标指令集。从那以后我每次拿到一个新的编译器实验包都强制先跑通 makefile 和 sample 对比再去看代码细节。希望帮到你。本文还有配套的精品资源点击获取