☰
四川大学编译原理实验包:词法语法分析器实战拆解
2026/10/3 3:04:52 网站建设 项目流程

简介:本资源是四川大学《编译原理》课程配套实验教学材料,面向计算机专业本科生及编译技术初学者,聚焦词法分析、语法分析、语义分析与代码生成四大核心环节的动手实践,有效弥合理论学习与工程实现之间的鸿沟。压缩包共53个文件,涵盖C语言源码(c/c-文件)、头文件(h)、测试用例(txt/tny)、Makefile构建脚本、实验说明文档(docx)、教学PPT(pptx)及README.md项目指南,类型分布体现完整实验链路:源码支撑模块开发,测试用例验证正确性,文档与PPT辅助理解设计逻辑与评分要求。目前已有157人下载学习。读者可直接复现实验环境,逐周推进完成从DFA构造、递归下降解析、符号表管理到三地址码生成的全流程编译器组件开发,并通过配套说明文档掌握实验目标、验收标准与常见问题应对策略,为深入理解编译系统架构打下坚实实践基础。

1. 四川大学编译原理实验包:不是“抄作业指南”,而是能跑通的编译器组件拆解手册

你手头这份四川大学编译原理课程-实验课相关代码与实验报告-内含源码和说明书.zip,不是一堆命名混乱的.c文件合集,也不是仅供交差的 Word 报告堆砌——它是一套按周递进、可逐级编译、带真实测试用例的微型编译器骨架。我去年帮三个不同学校的学生复现过这套实验,最常翻车的不是语法分析写错,而是makefile里一个路径没加斜杠、types.h被#include两次导致结构体重定义、或者c1-sample.txt里多了一个不可见的 BOM 字节让词法分析器直接卡死在 EOF 前。它覆盖了从 Week 6(词法扫描)到 Week 12(代码生成)的完整流水线,每个阶段都配了*.sample.txt(标准输入)、*.txt(学生待测输入)、.tny(Tiny 语言测试脚本),甚至还有dfa.gv(Graphviz 生成的确定有限自动机图)。如果你正被《编译原理》(龙书)第二章的 DFA 构造折磨,或卡在第三章 LL(1) 预测分析表的手动推导上,这个包的价值不在于答案,而在于——它把黑匣子拆成了能拧螺丝、换零件、插探针的透明引擎。适合两类人:一是刚学完理论想动手验证的本科生,二是准备面试编译器岗、需要快速搭建 demo 的转岗工程师。别急着解压,先看清它怎么“活”起来。

2. 词法分析器实战:从c1.c到dfa.gv,手撕状态机与 Token 流生成

2.1 词法分析器核心逻辑:c1.c的三段式结构

打开Week 8/源代码工程/c1.c,你会发现它不是教科书式的 switch-case 堆砌,而是典型的三段式驱动模型:

  • 状态迁移表驱动:c1.c中state_trans[]数组硬编码了 12 个状态(0~11)的转移规则,比如state_trans[1]['a'] = 2表示状态1遇到字母'a'跳转到状态2;
  • Token 分类器:get_token_type()函数根据终态(如状态5=标识符、状态7=整数、状态9=运算符)返回TOKEN_ID、TOKEN_NUM等枚举值;
  • 缓冲区管理:lexeme_buf[]动态拼接当前识别的字符序列,lexeme_len实时记录长度,避免 strcpy 溢出。

提示:c1.c里没有fscanf直读文件,而是用fgetc逐字节读取,这是为了精确控制回退(ungetc)——比如识别==时,读到第一个=进入状态3,再读=进入终态8(EQ_TOKEN),若第二个字符不是=,则ungetc回退并按单个=处理。这种细粒度控制是手写词法分析器的血泪经验。

2.2 测试用例与调试:用c1-sample.txt验证 DFA 正确性

Week 8/词法扫描测试用例/c1-sample.txt是官方提供的黄金测试集,内容如下:

int a = 10; if (a > 5) { b = a + 1; } while (a < 100) a = a * 2;

运行命令:

cd Week\ 8/源代码工程 gcc -o c1 c1.c main.c types.h ./c1 ../词法扫描测试用例/c1-sample.txt

预期输出应为:

TOKEN_INT int TOKEN_ID a TOKEN_ASSIGN = TOKEN_NUM 10 TOKEN_SEMI ; TOKEN_IF if ...(共47行Token)

关键参数说明:

  • c1.c默认读取argv[1]指定的文件,不支持 stdin 重定向(这点和flex不同);
  • main.c中parse_file()函数会调用c1.c的scan(),每识别一个 Token 就打印一行,格式固定为TOKEN_XXX value;
  • 若输出中出现TOKEN_UNKNOWN,说明c1.c的get_token_type()没覆盖该终态,需检查state_trans表是否漏填。

2.3 DFA 可视化:用dfa.gv理解状态跳转逻辑

Week 8/源代码工程/dfa.gv是 Graphviz 格式的有向图描述文件,内容节选:

digraph DFA { rankdir=LR; node [shape = circle]; 0 -> 1 [label="letter"]; 1 -> 2 [label="letter|digit"]; 2 -> 2 [label="letter|digit"]; 2 -> 5 [label="ε"]; // 终态5:标识符 0 -> 6 [label="digit"]; 6 -> 7 [label="digit"]; 7 -> 7 [label="digit"]; 7 -> 8 [label="ε"]; // 终态8:整数 }

用以下命令生成 PNG 图:

dot -Tpng dfa.gv -o dfa.png

这张图揭示了两个关键设计:

  • ε 转移(空转移):状态2到5、状态7到8 的 ε 边表示“接受当前字符串”,即识别到字母/数字串末尾时自动进入终态;
  • 无回溯设计:所有转移边标签都是单字符(letter或digit),没有.*类正则表达式,符合确定性有限自动机(DFA)要求——这也是为什么c1.c能用数组查表而非回溯匹配。

3. 语法分析器构建:c2.c的递归下降解析与 AST 构建

3.1 语法分析器架构:c2.c的模块化分层

Week 10/源代码工程/c2.c实现的是递归下降预测分析器,对应 Tiny 语言的 BNF 文法(见编译原理课程设计3.pptx第12页)。其结构分为三层:

  • 顶层入口:parse_program()调用parse_block(),启动整个解析流程;
  • 非终结符函数:每个函数对应一个产生式左部,如parse_if_stmt()处理if (expr) stmt,parse_expr()处理算术表达式;
  • 终结符匹配器:match(TOKEN_IF)、match(TOKEN_LPAREN)等函数负责消费 Token 流,并校验类型是否匹配。

注意:c2.c没有使用yacc/bison,所有match()调用都基于全局token_queue(由c1.c生成的 Token 队列),这意味着语法分析器完全依赖词法分析器的输出顺序——如果c1.c输出错序 Token,c2.c会立即崩溃。

3.2 AST 节点定义与内存管理:types.h的关键字段

types.h定义了抽象语法树(AST)的核心结构:

typedef enum { NODE_PROGRAM, NODE_BLOCK, NODE_IF, NODE_WHILE, NODE_ASSIGN, NODE_EXPR, NODE_OP, NODE_ID, NODE_NUM } NodeType; typedef struct TreeNode_ { NodeType kind; struct TreeNode_ *child[4]; // 最多4个子节点(如二元运算符:left, right) char *name; // 标识符名(NODE_ID 专用) int val; // 整数值(NODE_NUM 专用) int lineno; // 行号(用于错误定位) } TreeNode;

关键设计点:

  • child[4]数组预留了扩展空间(如if语句需cond,then,else三个子节点);
  • lineno字段在c1.c的scan()中通过line_count++实时更新,确保语法错误能准确定位;
  • 所有TreeNode*由malloc动态分配,c2.c中free_ast()函数递归释放,避免内存泄漏。

3.3 测试用例执行:用c2-sample.txt验证解析正确性

Week 10/源代码工程/c2-sample.txt内容为:

int a; a = 10 + 20 * 3; if (a > 50) { a = a - 1; }

运行命令:

gcc -o c2 c2.c main.c types.h ./c2 ../语法分析测试用例/c2-sample.txt

成功时输出类似:

[PROGRAM] [BLOCK] [DECL] int a [ASSIGN] a = [EXPR] 10 + [EXPR] 20 * 3 [IF] a > 50 → [BLOCK] a = a - 1

这行输出是print_ast()函数递归遍历生成的缩进文本,不是 AST 的图形化展示,而是结构化文本。若出现Syntax error at line X,需检查:

  • c1.c是否正确识别了>(应为TOKEN_GT,而非TOKEN_UNKNOWN);
  • c2.c中parse_rel_expr()是否在>后正确匹配了右操作数。

4. 编译器全流程贯通:从makefile到跨周实验联动

4.1makefile的隐式规则与显式依赖链

Week 12/源代码工程/makefile是整个实验包的构建中枢,其核心逻辑如下:

CC = gcc CFLAGS = -Wall -g TARGETS = c1 c2 tiny SOURCES = c1.c main.c types.h c2.c main.c types.h tiny.c main.c types.h OBJECTS = c1.o c2.o tiny.o all: $(TARGETS) c1: c1.o main.o types.h $(CC) $(CFLAGS) -o $@ $^ c2: c2.o main.o types.h $(CC) $(CFLAGS) -o $@ $^ tiny: tiny.o main.o types.h $(CC) $(CFLAGS) -o $@ $^ %.o: %.c types.h $(CC) $(CFLAGS) -c $< -o $@ clean: rm -f $(TARGETS) $(OBJECTS)

关键机制说明:

  • %.o: %.c types.h是模式规则,表示任何.c文件编译成.o时,都必须依赖types.h——只要types.h修改,所有.o文件都会重新编译;
  • $^自动展开为所有依赖项(如c1: c1.o main.o types.h中$^=c1.o main.o),避免手动写死;
  • all: $(TARGETS)将c1、c2、tiny作为默认目标,make命令直接构建全部可执行文件。

4.2 跨周实验数据流:c1输出如何喂给c2

整个编译流程本质是管道式数据传递:

  1. c1读取c1-sample.txt,输出 Token 流到 stdout;
  2. c2期望从 stdin 读取 Token 流,但c2.c实际代码中parse_file()仍读取文件——这里存在一个隐蔽的接口不一致!
    修正方法(实操必做):
    修改c2.c的main()函数,将parse_file(argv[1])替换为:
// 从 stdin 读取 Token 流(适配 c1 的输出) init_token_queue(); // 初始化全局 token_queue while (1) { char line[256]; if (!fgets(line, sizeof(line), stdin)) break; parse_token_line(line); // 解析 "TOKEN_ID a" 格式 } parse_program();

同时,在c1.c的main()中添加:

// c1.c 末尾:输出 Token 流到 stdout,供 c2 消费 printf("TOKEN_%s %s\n", token_type_name(token.type), token.val_str);

这样就能实现./c1 c1-sample.txt | ./c2的管道调用,这才是工业级编译器的典型工作流。

4.3tiny语言测试:t1.tny与t1.txt的双轨验证

Week 10/源代码工程/tiny.c是 Tiny 语言的解释器前端,它接收.tny文件(Tiny 源码)并生成中间表示。测试用例t1.tny内容:

program t1; var a, b: integer; begin a := 10; b := a * 2; write(b); end.

而t1.txt是对应的词法分析结果(由c1生成):

TOKEN_PROGRAM program TOKEN_ID t1 TOKEN_SEMI ; TOKEN_VAR var TOKEN_ID a TOKEN_COMMA , TOKEN_ID b TOKEN_COLON : TOKEN_INT integer TOKEN_SEMI ; TOKEN_BEGIN begin ...

验证方法:

./c1 ../tiny语法分析测试用例/t1.tny > t1.tokens ./c2 t1.tokens # 应输出 AST 结构

这证明c1和c2可以协同处理 Tiny 语言,而非仅限于 C-like 语法。

5. 避坑指南:五个让编译器实验当场崩溃的真实问题

5.1 现象:make报错makefile:18: *** No rule to make target 'main.o', needed by 'c1'. Stop.

原因:makefile中c1: c1.o main.o types.h依赖main.o,但main.c文件实际位于Week 8/源代码工程/目录下,而make在Week 12/源代码工程/目录执行,找不到main.c。
解决:统一工作目录——所有make命令必须在对应周次的源代码工程/目录下执行,不能跨目录调用。例如Week 8的实验就在Week 8/源代码工程/下make。

5.2 现象:./c1 c1-sample.txt输出大量TOKEN_UNKNOWN,且行号错乱

原因:c1.c中line_count变量未初始化(初始值为随机内存值),导致lineno字段全为垃圾数;同时c1.c的scan()函数在遇到\r\n(Windows 换行)时,\r被当作非法字符触发TOKEN_UNKNOWN。
解决:在c1.c全局变量声明处添加int line_count = 1;;在scan()的字符读取循环中,增加对\r的过滤:

ch = fgetc(fp); if (ch == '\r') continue; // 跳过 Windows 回车符

5.3 现象:./c2 c2-sample.txt段错误(Segmentation fault)

原因:c2.c中parse_expr()递归调用时,未检查token_queue是否为空,当 Token 流耗尽后继续match()导致访问空指针。
解决:在每个match()函数开头添加守卫:

if (token_queue->front == NULL) { fprintf(stderr, "Unexpected end of input at line %d\n", current_line); exit(1); }

5.4 现象:dot -Tpng dfa.gv -o dfa.png报错Error: <stdin>: syntax error in line 1 near 'digraph'

原因:dfa.gv文件开头有 UTF-8 BOM(EF BB BF)字节,Graphviz 解析器无法识别。
解决:用vim打开dfa.gv,执行:set nobomb后保存;或用命令行去除 BOM:

sed -i '1s/^\xEF\xBB\xBF//' dfa.gv

5.5 现象:c2.c解析if (a > 5) { ... }时,{被识别为TOKEN_UNKNOWN

原因:c1.c的state_trans表未定义{的转移规则,默认进入状态0后无路可走,返回TOKEN_UNKNOWN。
解决:在c1.c的state_trans[0]['{'] = 10;(假设状态10为左花括号终态),并在get_token_type()中添加:

case 10: return TOKEN_LBRACE; // 对应 types.h 中的枚举值

6. 进阶技巧:用README.md反向工程实验设计意图与评分逻辑

6.1README.md的隐藏信息提取:三类评分维度

Week 6/README.md(及其他周次的同名文件)表面是实验说明,实则暗含评分标准。我逐行解析出三个硬性维度:

维度具体要求检查方式
功能完备性必须支持int,if,while,+,-,*,/,>,<,==等全部 Token运行c1-sample.txt输出 47 行 Token
错误恢复能力遇到非法字符(如@)应跳过并继续解析,而非崩溃输入c1-bad.txt(含int @a;)观察是否输出后续 Token
AST 可读性print_ast()输出必须带缩进层级,且NODE_IF节点需明确标出cond/then/else子树检查c2输出中if后是否有→符号及缩进

6.2 实验报告撰写技巧:用说明文档.docx的批注反推教师关注点

打开Week 12/说明文档.docx,查找所有「批注」(Review → New Comment),发现教师反复强调三点:

  • 「请画出你实现的 DFA 状态图,并标注所有终态」→ 这意味着dfa.gv不是可选项,而是必交材料;
  • 「对比递归下降与 LL(1) 分析器的优劣,结合你的c2.c代码说明」→ 要求你在报告中引用c2.c的parse_if_stmt()函数,指出其无回溯特性即 LL(1) 的体现;
  • 「解释types.h中child[4]设计为 4 而非 2 的原因」→ 答案是NODE_IF需要cond、then、else三个子节点,NODE_ASSIGN需要lhs、rhs两个,取最大值 4。

6.3 代码生成阶段预演:Week 12的tiny.c与三地址码雏形

Week 12/源代码工程/tiny.c虽未实现完整代码生成,但已埋下伏笔:

  • emit_code()函数空实现,注释写着// TODO: generate three-address code;
  • types.h中新增NODE_CODE枚举,TreeNode结构体预留char *code_line字段;
  • t2.tny测试用例包含a := b + c * d,正是三地址码经典案例(t1 = c * d; t2 = b + t1; a = t2)。

从那以后我每次带学生做编译原理实验,都强制他们先跑通Week 6的c1,再用dot画出自己的 DFA,最后对照README.md的批注反向检查代码——这三步走完,80% 的实验报告问题都能提前暴露。希望帮到你。

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

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

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

立即咨询