☰
川大编译原理实验包:词法语法语义代码生成全阶段代码与报告
2026/10/12 16:00:56 网站建设 项目流程

简介:这份资源是四川大学编译原理课程的实验课配套资料,面向正在学习编译原理、需要动手实现编译器各阶段组件的高校学生。内容按周次组织,覆盖词法分析、语法分析、语义分析与代码生成等核心环节,并配有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_tail,expr_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。

# 安装 Graphviz(Ubuntu/Debian) sudo apt-get install graphviz # 渲染 dfa.gv 为 PNG 图片 dot -Tpng dfa.gv -o dfa.png # 如果 dfa.gv 里有中文标签,指定字体 dot -Tpng -Gfontname="SimHei" 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 对比,再去看代码细节。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询