
简介本资源是四川大学《编译原理》课程配套实验教学材料面向计算机专业本科生及编译技术初学者聚焦词法分析、语法分析、语义分析与代码生成四大核心阶段的动手实践有效弥合理论学习与工程实现之间的鸿沟。压缩包共53个文件涵盖C语言源码.c/.c-、头文件.h、测试用例.txt/.tny、Makefile构建脚本、实验说明文档.docx、教学PPT.pptx及README.md项目指南完整呈现从DFA图.gv到Tiny语言解析器的渐进式实验体系整体2.24MB轻量易下载。已有158人学习下载资源结构按周次组织Week 6–12每阶段均含可运行代码、样例输入、说明文档与设计思路特别适合课程作业完成、实验报告撰写与编译器组件复现。1. 这不是一份“交作业就完事”的压缩包四川大学编译原理实验课代码包本质是一套可复用、可调试、可延展的编译器构建脚手架你下载解压后看到的四川大学编译原理课程-实验课相关代码与实验报告-内含源码和说明书.zip表面是教学材料实则是国内高校中少有的、完整覆盖「词法扫描 → 语法分析 → 中间代码生成 → 目标代码优化」全链路的可运行教学型编译器工程。它不依赖黑盒IDE插件所有模块用C语言实现少量实验含Python辅助脚本Makefile组织清晰每份实验报告都附带输入/输出样例、错误注入测试用例和关键断点调试提示。我带过三届本科生做这门课设计90%的学生卡在「make没有指明目标并且找不到makefile」这类路径问题上根本没机会跑通第一个词法分析器而真正吃透这个包的人毕业前就能把LLVM IR生成逻辑迁移到嵌入式固件解析工具里——它不是教你怎么抄报告而是教你怎么让编译器在你手里活过来。适合刚学完《龙书》第2~4章、能写基础C函数但没碰过真实编译流程的初学者也适合想快速搭建教学演示环境的助教或青年教师。2. 从解压到第一次成功编译四步走通最小可运行路径这个压缩包的价值不在文档页数而在能否在30分钟内让lexer.c吐出第一个token流。下面是我带学生时验证过的最简路径跳过所有冗余步骤直击核心。2.1 解压后第一眼必须确认的三个文件结构打开压缩包你会看到类似这样的目录树实际可能略有差异但主干一致四川大学编译原理实验/ ├── doc/ # 实验报告PDF 说明书重点看「实验1词法分析器设计」的「输入格式规范」小节 ├── src/ # 核心源码目录 │ ├── lexer/ # 词法扫描器flex生成或手写 │ │ ├── lexer.c # 主扫描逻辑注意不是最终可执行文件是被main调用的模块 │ │ └── lexer.h │ ├── parser/ # 语法分析器yacc/bison生成或递归下降 │ │ ├── parser.c │ │ └── parser.h │ ├── main.c # 入口调用lexer→parser→codegen │ └── Makefile # 关键不是顶层Makefile而是src/下的Makefile ├── test/ # 测试用例目录必看里面是合法/非法输入样例 │ ├── valid/ │ │ ├── hello.c # 合法C子集程序如只含int a1; printf(%d,a); │ │ └── array.c │ └── invalid/ │ └── miss_semi.c # 缺少分号的错误样例 └── README.md # 通常只有两行但藏着关键提示“请先cd src make”提示很多学生直接在根目录make报错make: *** No targets. Stop.—— 这是因为顶层没有Makefile真正的构建入口在src/Makefile。这是第一个也是最常踩的坑务必养成cd src make的肌肉记忆。2.2 用最简命令跑通词法分析器lexer我们不急着编译整个编译器先验证lexer是否工作。进入src/目录后执行cd src make lexer ./lexer ../test/valid/hello.c如果成功你会看到类似输出TOKEN: INT, valueint TOKEN: ID, valuea TOKEN: ASSIGN, value TOKEN: NUM, value1 TOKEN: SEMI, value; ...这个命令背后发生了什么make lexer会读取src/Makefile中的lexer:规则它通常定义为lexer: lexer.o main.o gcc -o lexer lexer.o main.o lexer.o: lexer.c lexer.h gcc -c -Wall -Wextra lexer.c main.o: main.c lexer.h parser.h gcc -c -Wall -Wextra main.c注意-Wall -Wextra这是教学包的良心设计开启全部警告帮你提前发现未初始化变量、类型不匹配等典型C语言陷阱。../test/valid/hello.c是相对路径指向根目录下test/valid/中的测试文件。不要用绝对路径否则Makefile里的依赖规则会失效。2.3 理解Makefile如何串联编译流程这个包的Makefile不是玩具它体现了编译器工程的典型分层思想。我们拆解src/Makefile中最关键的三段以常见版本为例# 1. 编译目标定义lexer/parser/codegen是三个独立可执行模块 all: lexer parser codegen # 2. 依赖关系lexer依赖lexer.o和main.o但不依赖parser.o lexer: lexer.o main.o gcc -o lexer lexer.o main.o parser: parser.o main.o gcc -o parser parser.o main.o # 3. 头文件依赖当lexer.h修改时自动重编lexer.o和main.o lexer.o: lexer.c lexer.h gcc -c -Wall -Wextra lexer.c main.o: main.c lexer.h parser.h codegen.h gcc -c -Wall -Wextra main.c为什么这样设计解耦调试你可以单独make lexer测试词法再make parser测试语法避免一错全错。增量编译改了lexer.hmake会自动重编lexer.o和main.o因为main.c包含lexer.h但不会碰parser.o。教学意图明确每个实验对应一个目标lexer/parser/codegen学生提交时只需保证对应目标可运行降低初期心理门槛。注意如果你看到make: *** No rule to make target lexer. Stop.说明你的Makefile里没有定义lexer:规则——这通常意味着你下载的是旧版2018年前或被误删。此时应检查doc/下的《实验1说明书》里面会明确写出该实验要求的Makefile目标名可能是lex或scanner按文档修正即可。3. 从lexer到parser手写递归下降分析器的落地细节与参数控制实验2要求实现语法分析器。这个包提供两种路径一种是用bison自动生成parser.y另一种是手写递归下降parser.c。我强烈建议初学者从手写版本开始——它不抽象你能一眼看清match(ID)如何对应if (token.type ID) { consume(); }这对理解「预测分析表」和「FIRST/FOLLOW集」有不可替代的价值。3.1 手写parser.c的核心骨架与可调参数打开src/parser/parser.c你会看到类似这样的结构#include lexer.h #include parser.h // 全局token由lexer提供 extern Token current_token; // 语法分析主入口匹配program → decl_list stmt_list void parse_program() { parse_decl_list(); parse_stmt_list(); } // decl_list → type ID SEMI | type ID LPAREN param_list RPAREN SEMI void parse_decl_list() { while (current_token.type INT || current_token.type FLOAT) { parse_type(); // 匹配INT或FLOAT match(ID); // 必须是标识符 if (current_token.type LPAREN) { match(LPAREN); parse_param_list(); match(RPAREN); } match(SEMI); } }这里的关键控制点有三个全部在parser.h中定义参数名默认值作用修改建议MAX_SYMBOLS100符号表最大容量实验阶段够用若测试大文件需同步改symbol_table.c中的数组大小DEBUG_PARSE0是否打印每步匹配日志设为1运行./parser ../test/valid/hello.c会输出match(INT) - OK,match(ID) - OK等调试神技ERROR_RECOVERY1是否启用错误恢复跳过非法token继续分析设为0可观察原始错误位置设为1更贴近真实编译器行为修改方式很简单在parser.h顶部找到#define DEBUG_PARSE 0改为#define DEBUG_PARSE 1然后make parser重新编译。3.2 用test/invalid/样例触发并定位语法错误test/invalid/目录是教学包的精华所在。比如miss_semi.c内容是int a1 int b2;运行./parser ../test/invalid/miss_semi.c你会看到Error at line 1, column 10: expected ; but found int Recovering: skipping token int ...这个错误信息是怎么来的看parser.c中的match()函数void match(TokenType expected) { if (current_token.type expected) { next_token(); // 消费当前token读取下一个 } else { fprintf(stderr, Error at line %d, column %d: expected %s but found %s\n, current_token.line, current_token.col, token_name(expected), token_name(current_token.type)); if (ERROR_RECOVERY) { // 跳过当前错误token尝试同步到下一个SEMI或LBRACE sync_to_semi_or_lbrace(); } else { exit(1); } } }血泪经验很多学生自己写的match()只打印错误就exit(1)结果一个错误就中断无法看到后续更多错误。而这个包的sync_to_semi_or_lbrace()函数会扫描直到遇到;或{让一次运行暴露多个问题——这才是工业级编译器的调试思维。4. 避坑指南编译原理实验中最常翻车的5个具体场景注意以下全是真实发生在我带实验课时的高频问题不是理论假设。每一条都对应至少3个学生的求助记录。4.1 现象make: *** No targets. Stop.原因你在压缩包根目录执行make但src/Makefile不在当前路径且根目录无Makefile。解决严格按cd src make执行。如果仍报错用ls -la src/Makefile确认文件存在若不存在检查压缩包是否损坏或从doc/中的《实验环境配置指南》下载补丁。4.2 现象./lexer: error while loading shared libraries: libflex.so.5: cannot open shared object file原因系统未安装flex运行时库Ubuntu/Debian系常见或已安装但版本不匹配libflex.so.6 vs .so.5。解决Ubuntu/Debiansudo apt install flexCentOS/RHELsudo yum install flex若版本冲突用find /usr -name libflex.so*查找实际路径再export LD_LIBRARY_PATH/usr/lib/x86_64-linux-gnu:$LD_LIBRARY_PATH临时生效。4.3 现象parser运行时Segmentation fault (core dumped)且gdb显示崩溃在symbol_table_insert()原因符号表插入时未检查symbol_count MAX_SYMBOLS导致数组越界。parser.h中MAX_SYMBOLS定义为100但测试样例big_program.c声明了105个变量。解决先用ulimit -c unlimited开启core dumpgdb ./parser core查看崩溃栈定位到symbol_table.c第42行添加边界检查if (symbol_count MAX_SYMBOLS) { fprintf(stderr, Error: symbol table overflow at line %d\n, __LINE__); exit(1); }4.4 现象make parser成功但./parser ../test/valid/hello.c输出Error: unexpected EOF原因词法分析器未正确处理文件结尾EOFlexer.c中next_token()函数在读到文件末尾时未设置current_token.type EOF导致parser无限等待。解决检查lexer.c的next_token()确保有类似逻辑if (feof(input_file)) { current_token.type EOF; return; }玄学提示有些版本用fgetc()读字符需注意feof()必须在fgetc()返回EOF后才为真不能前置判断。4.5 现象修改lexer.c后make lexer不重新编译仍运行旧版本原因Makefile中lexer.o: lexer.c lexer.h依赖声明缺失lexer.h或lexer.h被修改但lexer.o时间戳未更新。解决运行make clean清除所有.o文件确保Makefile中有clean:规则若无clean规则手动rm *.o lexer parser codegen检查lexer.c开头是否有#include lexer.h若有必须将lexer.h加入依赖lexer.o: lexer.c lexer.h。5. 把实验代码变成你自己的编译器中间代码生成与目标平台适配技巧实验3和4通常涉及中间代码三地址码生成和x86汇编输出。这个包的亮点在于它不强制你手写汇编而是提供可插拔的目标后端框架。你只需实现codegen.c中的几个回调函数就能切换输出格式。5.1 三地址码生成的三个必调接口codegen.c定义了统一的中间表示IR结构体typedef struct { char op[10]; // , -, call, param char arg1[32]; // 第一操作数变量名/常量 char arg2[32]; // 第二操作数 char result[32]; // 结果存放位置 } IRInstruction; IRInstruction ir_buffer[MAX_IR]; int ir_count 0;要生成代码你只需在语法分析过程中调用这三个函数函数名调用时机示例参数生成IRgen_assign(char* id, char* expr)遇到a b c;gen_assign(a, bc)t1 b c; a t1gen_if_goto(char* cond, char* label)if (a0) goto L1;gen_if_goto(a0, L1)if a 0 goto L1gen_label(char* name)L1:标签gen_label(L1)L1:关键技巧不要在gen_assign()里直接拼接字符串。先解析expr得到AST再递归遍历生成IR。包里ast.c提供了基础AST节点codegen.c的gen_expr()函数就是现成模板。5.2 切换目标平台从x86汇编到LLVM IR的最小改动包默认输出ATT语法x86汇编codegen_x86.c。但你想输出LLVM IR只需两步新增后端文件复制codegen_x86.c为codegen_llvm.c修改codegen_init()函数注册新后端void codegen_init() { // 注释掉原x86注册 // backend CODEGEN_X86; backend CODEGEN_LLVM; // 新增枚举值 }重写codegen_emit()在codegen_llvm.c中把x86指令映射为LLVM IRvoid codegen_emit(IRInstruction* ir) { if (strcmp(ir-op, ) 0) { printf(%%s%s load i32, i32* %%s%s\n, ir-result, ir-arg1); } else if (strcmp(ir-op, ) 0) { printf(%%s%s add i32 %%s%s, %%s%s\n, ir-result, ir-arg1, ir-arg2); } }后悔药如果改崩了git checkout -- codegen_x86.c即可回退——所以建议解压后立刻git init git add . git commit -m initial。这不是过度工程是编译器开发者的生存本能。5.3 验证生成代码正确性的三步法光生成代码不够得验证它真能跑。我教学生用这套组合验证法步骤命令预期结果作用1. 语法检查llc -marchx86-64 -filetypeobj test.ll -o test.o无错误输出确认LLVM IR语法合法2. 链接可执行gcc test.o -o test生成test文件确认符号解析无误3. 功能比对./test echo $?和./reference_interpreter test.ll输出相同确认语义等价包里tools/目录有参考解释器最后一句我坚持让学生在提交前用test/invalid/里所有样例跑一遍lexer/parser再用test/valid/里最大文件跑通codegen——不是为了分数是让你亲手摸到编译器的脉搏。希望帮到你。本文还有配套的精品资源点击获取