简介:这份资源是电子科技大学编译原理课程的实验代码合集,面向正在学习编译原理、需要动手实现词法分析与语法分析的高校学生及自学者。内容围绕编译器前端核心模块展开,包含词法分析器与语法分析器的完整实现,涉及正则表达式、有限状态自动机、LL/LR解析策略以及抽象语法树构建等关键知识点,并配有运行说明文档与可执行程序,便于对照验证理论到实践的落地过程。压缩包共21个文件,约203KB,以cpp与h源码为主体,另有pas示例、docx说明文档及vcxproj、sln等工程配置,结构覆盖输入处理、词法分析、语法分析等模块,目录组织清晰。目前已有2111人学习下载,适合作为课程实验参考、满分代码研读与编译器开发入门的实践素材,帮助读者理解token流生成、语法规则解析及工程组织方式。
1. 电子科技大学编译原理实验代码:从词法分析到代码生成的完整复现路径
如果你正在搜「电子科技大学编译原理实验代码」,大概率不是想抄一份交差,而是卡在了某个环节——词法分析器的正则表达式写不对,语法树的节点结构设计混乱,或者语义分析里的符号表越写越乱。这门课的实验通常要求学生从零实现一个简化编译器,覆盖词法分析、语法分析、语义分析、中间代码生成几个核心阶段。网上流传的「编译原理实验代码」质量参差不齐,很多只贴了片段,缺少构建方式和测试用例,拿过来跑不通。这篇内容按我实际带学生做实验的经验,把每个阶段的实现思路、关键参数、常见翻车点拆开讲,代码用 C++ 和 Flex/Bison 混合方案,因为这是电子科技大学该课程最常用的技术栈。读完你能自己搭出一套可编译、可测试、可扩展的实验代码框架,而不是复制一堆跑不起来的死代码。
2. 实验环境搭建与词法分析器实现
2.1 工具链选型:为什么用 Flex + Bison 而不是手写递归下降
电子科技大学编译原理实验通常指定 C/C++ 作为实现语言,工具链上一般提供两个方向:纯手写递归下降,或者用 Flex 做词法、Bison 做语法。我建议词法阶段用 Flex,语法阶段用 Bison,原因是实验的评分点集中在「能否正确处理复杂文法」和「错误恢复能力」,手写词法分析器在正则匹配和缓冲区管理上容易出 bug,而 Flex 生成的 DFA 在性能和正确性上都更稳。Flex 的规则文件以.l为后缀,Bison 用.y,两者通过 token 定义头文件衔接。
安装命令在 Ubuntu 下很直接:
sudo apt update sudo apt install flex bison gcc g++ make flex --version bison --version版本上 Flex 2.6.4 和 Bison 3.8 以上都兼容,不需要追最新。注意 Bison 从 3.0 开始默认生成 C++ 兼容代码的方式有变化,如果实验指导书用的是老版本示例,编译时可能报yylex签名不匹配,这个后面避坑章节会细说。
2.2 词法规则文件 lexer.l 的完整写法与参数说明
下面是一个能识别整数、浮点数、标识符、关键字、运算符和注释的词法规则文件。关键字表用哈希或简单的字符串比较都行,实验规模下直接 if-else 链足够。
%{ #include <string> #include <iostream> #include "token.h" int line_num = 1; %} %option noyywrap DIGIT [0-9] ID [a-zA-Z_][a-zA-Z0-9_]* FLOAT {DIGIT}+\.{DIGIT}+ %% "int"|"float"|"if"|"else"|"while"|"return" { return KEYWORD; } {FLOAT} { yylval.fval = atof(yytext); return FLOAT_LIT; } {DIGIT}+ { yylval.ival = atoi(yytext); return INT_LIT; } {ID} { yylval.sval = strdup(yytext); return IDENTIFIER; } "+"|"-"|"*"|"/" { return yytext[0]; } "=="|"!="|"<="|">=" { return COMPARE_OP; } [ \t]+ { /* 忽略空白 */ } \n { line_num++; } "//".* { /* 忽略单行注释 */ } "/*"([^*]|\*+[^*/])*\*+"/" { /* 忽略多行注释 */ } . { printf("Lexical error at line %d: %s\n", line_num, yytext); } %%逻辑说明:%option noyywrap让 Flex 不依赖-lfl库,方便直接链接。yylval是 Bison 定义的语义值联合体,需要在token.h里声明。浮点数规则必须放在整数规则前面,否则3.14会被拆成3、.、14三个 token,这是血泪经验。多行注释的正则([^*]|\*+[^*/])*\*+"/"是标准写法,能正确处理/**/和/* ** */这类嵌套星号的情况。
参数上,line_num用于错误定位,实验报告里通常要求输出行号。strdup分配的内存要在语法分析阶段释放,否则长时间运行会泄漏,虽然实验规模小,但养成习惯没坏处。
2.3 编译与测试词法分析器的具体命令
写一个简单的main.cpp调用yylex()循环打印 token:
#include "token.h" extern int yylex(); extern int line_num; int main() { int tok; while ((tok = yylex()) != 0) { printf("Token: %d, line: %d\n", tok, line_num); } return 0; }编译命令:
flex -o lexer.yy.cpp lexer.l g++ -c lexer.yy.cpp -o lexer.o g++ -c main.cpp -o main.o g++ lexer.o main.o -o lexer_test ./lexer_test < test.c如果报undefined reference to yylval,说明token.h里没有定义YYSTYPE,补上extern YYSTYPE yylval;即可。测试用例至少覆盖:纯整数运算、浮点混合、嵌套注释、非法字符(如@),观察错误输出是否带行号。
3. 语法分析:用 Bison 构建 AST 并处理优先级
3.1 文法设计与 AST 节点结构
语法分析阶段的核心是把 token 流变成抽象语法树。电子科技大学实验通常要求支持表达式、赋值、if-else、while 和函数定义。文法用 Bison 的 BNF 写,优先级用%left、%right声明,避免产生移进-归约冲突。
AST 节点建议用继承体系,基类ASTNode带virtual void codegen()或virtual int eval(),子类分ExprNode、StmtNode、DeclNode。下面是一个精简的节点定义:
struct ASTNode { virtual ~ASTNode() = default; virtual void dump(int indent = 0) = 0; }; struct BinaryExpr : ASTNode { std::string op; ASTNode *left, *right; BinaryExpr(std::string o, ASTNode *l, ASTNode *r) : op(o), left(l), right(r) {} void dump(int indent) override { printf("%*sBinaryExpr(%s)\n", indent, "", op.c_str()); left->dump(indent + 2); right->dump(indent + 2); } };参数说明:indent控制打印缩进,方便调试树结构。op存运算符字符串,left/right是子节点指针。内存管理上,实验阶段可以用new不释放,但更好的做法是用std::unique_ptr,不过 Bison 的语义动作里用裸指针更顺手,折中方案是在程序退出前统一遍历释放。
3.2 Bison 规则文件 parser.y 的关键片段
%{ #include "ast.h" extern int yylex(); extern int line_num; void yyerror(const char *msg); %} %union { int ival; double fval; char *sval; ASTNode *node; } %token <ival> INT_LIT %token <fval> FLOAT_LIT %token <sval> IDENTIFIER KEYWORD %token COMPARE_OP %type <node> expr stmt program %left '+' '-' %left '*' '/' %right UMINUS %% program: stmt_list { root = $1; } ; expr: expr '+' expr { $$ = new BinaryExpr("+", $1, $3); } | expr '-' expr { $$ = new BinaryExpr("-", $1, $3); } | expr '*' expr { $$ = new BinaryExpr("*", $1, $3); } | expr '/' expr { $$ = new BinaryExpr("/", $1, $3); } | '-' expr %prec UMINUS { $$ = new UnaryExpr("-", $2); } | '(' expr ')' { $$ = $2; } | INT_LIT { $$ = new IntLit($1); } | IDENTIFIER { $$ = new VarRef($1); } ; %%逻辑说明:%left和%right的顺序决定优先级,先声明的优先级低。%prec UMINUS给一元负号单独指定优先级,否则-3+4会被解析成-(3+4)。%union里同时放int、double、char*和ASTNode*,Bison 会自动按 token 类型取对应字段。
编译命令:
bison -d -o parser.tab.cpp parser.y flex -o lexer.yy.cpp lexer.l g++ -c parser.tab.cpp -o parser.tab.o g++ -c lexer.yy.cpp -o lexer.yy.o g++ -c main.cpp -o main.o g++ parser.tab.o lexer.yy.o main.o -o compiler-d生成parser.tab.hpp,里面包含 token 枚举和YYSTYPE定义,lexer.l里 include 这个头文件就能用 token 常量。
3.3 冲突排查:移进-归约冲突的定位与解决
Bison 编译时如果输出conflicts: 2 shift/reduce,说明文法有歧义。用bison -v parser.y生成parser.output,里面会列出具体状态和冲突项。常见原因是 if-else 的悬挂问题,解决办法是在%nonassoc里声明IF和ELSE的优先级,让 Bison 优先移进 else。另一个常见冲突是表达式文法没有分层,把expr和term混在一起写,按标准分层(expr -> term -> factor)能消除大部分冲突。
4. 语义分析与符号表:类型检查与作用域管理
4.1 符号表数据结构选型:哈希表 vs 栈式链表
语义分析阶段要维护符号表,记录变量名、类型、作用域层级。实验规模下,用std::unordered_map<std::string, Symbol>配合作用域栈是最省事的方案。每进入一个块({})压入新 map,退出时弹出。查找时从栈顶往下遍历,找到第一个匹配即返回。
struct Symbol { std::string name; std::string type; // "int" 或 "float" int scope_level; }; class SymbolTable { std::vector<std::unordered_map<std::string, Symbol>> scopes; public: SymbolTable() { scopes.emplace_back(); } void enterScope() { scopes.emplace_back(); } void exitScope() { scopes.pop_back(); } bool insert(const Symbol &s) { auto &top = scopes.back(); if (top.count(s.name)) return false; top[s.name] = s; return true; } Symbol* lookup(const std::string &name) { for (auto it = scopes.rbegin(); it != scopes.rend(); ++it) { auto found = it->find(name); if (found != it->end()) return &found->second; } return nullptr; } };参数说明:scope_level用于报错时提示变量定义位置。insert返回false表示重复定义,调用方据此输出错误。lookup从内层往外找,符合词法作用域规则。
4.2 类型检查的遍历实现与错误恢复
在 AST 上做一次后序遍历,每个节点返回自己的类型,父节点检查子节点类型是否匹配。比如BinaryExpr的+要求两边都是int或都是float,混用时报错并尝试隐式转换(实验通常要求报错即可,不要求自动转换)。
std::string BinaryExpr::checkType(SymbolTable &st) { std::string lt = left->checkType(st); std::string rt = right->checkType(st); if (lt != rt) { printf("Type error at line %d: %s vs %s\n", line, lt.c_str(), rt.c_str()); return "error"; } return lt; }错误恢复上,遇到类型错误不要直接exit,而是返回"error"类型继续遍历,这样一次编译能报出所有错误,实验评分里「错误恢复能力」通常占分。注意line字段需要在 AST 节点构造时从yylineno传入,否则报错没有行号。
4.3 作用域嵌套的测试用例设计
至少准备三个测试文件:全局变量与局部变量同名、内层块访问外层变量、内层块重复定义变量。预期输出分别是:正确解析、正确解析、报重复定义错误。测试时用diff对比预期输出和实际输出,避免肉眼漏看。
5. 中间代码生成与目标代码输出
5.1 三地址码的生成规则与临时变量管理
中间代码用三地址码,每条指令形如x = y op z。临时变量用t1、t2递增命名,用全局计数器管理。表达式a + b * c生成:
t1 = b * c t2 = a + t1生成函数挂在 AST 节点上,BinaryExpr::codegen先递归生成左右子节点的代码,再发射自己的指令。注意临时变量不要复用,实验阶段以正确性优先,寄存器分配是研究生阶段的内容。
std::string BinaryExpr::codegen() { std::string l = left->codegen(); std::string r = right->codegen(); std::string t = newTemp(); printf("%s = %s %s %s\n", t.c_str(), l.c_str(), op.c_str(), r.c_str()); return t; }参数说明:newTemp()返回"t" + std::to_string(++temp_count)。codegen返回的是存放结果的变量名,供父节点使用。
5.2 控制流语句的标签生成与回填
if-else 和 while 需要生成标签和跳转指令。用newLabel()生成L1、L2,在合适位置发射goto和条件跳转。while 的结构是:
L1: cond ifFalse goto L2 body goto L1 L2:回填(backpatching)用于布尔表达式短路求值,实验里如果只要求基本控制流,可以先不做回填,直接生成完整跳转。但电子科技大学的实验通常要求支持&&和||的短路,这时需要维护truelist和falselist,在遇到&&时回填左操作数的truelist到右操作数的开始位置。
5.3 从三地址码到汇编的简单映射
如果实验要求生成目标代码,通常选 MIPS 或 x86 汇编。以 MIPS 为例,三地址码t1 = a + b映射为:
lw $t0, a lw $t1, b add $t2, $t0, $t1 sw $t2, t1变量到寄存器的映射用简单的栈式分配:每个变量分配一个栈偏移,临时变量用$t0-$t9轮转。实验评分不要求寄存器优化,能正确运行即可。测试时用 SPIM 或 MARS 模拟器加载汇编文件,输入测试用例,对比输出。
6. 避坑与常见问题排查
6.1 Flex 与 Bison 版本不匹配导致 yylex 签名错误
现象:编译时报error: 'int yylex()' redeclared as different kind of symbol。原因:Bison 3.0 以上默认生成yylex(YYSTYPE*)或 C++ 类接口,而 Flex 生成的yylex()无参数。解决:在parser.y的%code块里加#define YYLEX_PARAM或在 Flex 文件里用%option bison-bridge,最省事的办法是统一用 C 接口,Bison 加%language "C",Flex 不加 C++ 选项。
6.2 语义值联合体未初始化导致随机崩溃
现象:语法分析偶尔崩溃,gdb 回溯显示yylval指向非法地址。原因:%union里的char*字段在未赋值时是随机值,某些规则分支没设置$$就返回。解决:在 Bison 的每个产生式里显式给$$赋值,或者在yyerror里打印当前 token 辅助定位。更彻底的办法是用%define api.value.type variant启用 C++ 变体,但改动较大,实验阶段手动检查更快。
6.3 符号表作用域退出时未清理导致内存泄漏
现象:长时间运行或大测试文件下内存持续增长。原因:exitScope只弹出了 map,但 map 里的Symbol如果持有new分配的字符串,没有释放。解决:Symbol里的name和type用std::string而非char*,让 RAII 自动管理。如果已经用了char*,在exitScope里遍历释放。
6.4 三地址码临时变量命名冲突
现象:嵌套表达式生成的临时变量名重复,导致后续指令覆盖前面结果。原因:newTemp的计数器在递归调用中被重置,或者多个编译单元共享计数器但没加static。解决:把temp_count定义为文件级static int,或者封装成单例类。测试时用a+b*c-d/e这种混合表达式,检查生成的临时变量是否唯一。
6.5 测试用例覆盖不全导致隐藏 bug
现象:简单用例通过,复杂用例报错。原因:只测了顺序执行,没测嵌套 if、while 内 break、递归函数调用。解决:按文法产生式逐条设计测试用例,每个非终结符至少一个正例一个反例。用脚本批量跑测试并对比预期输出,比手动快得多。
7. 进阶技巧:用脚本自动化测试与性能分析
实验代码写完后,手动跑测试用例效率太低。我一般写一个 Python 脚本,遍历tests/目录下的.c文件,调用编译器生成三地址码,再和expected/下的预期输出对比。脚本核心逻辑:
import subprocess, os, sys test_dir = "tests" expected_dir = "expected" fail = 0 for f in sorted(os.listdir(test_dir)): if not f.endswith(".c"): continue src = os.path.join(test_dir, f) exp = os.path.join(expected_dir, f.replace(".c", ".out")) result = subprocess.run(["./compiler"], stdin=open(src), capture_output=True, text=True) with open(exp) as ef: expected = ef.read() if result.stdout.strip() != expected.strip(): print(f"FAIL: {f}") print("Got:\n", result.stdout) print("Expected:\n", expected) fail += 1 else: print(f"PASS: {f}") sys.exit(1 if fail else 0)参数说明:capture_output=True捕获 stdout 和 stderr,text=True返回字符串而非字节。strip()忽略行尾空白差异。这个脚本能集成到 Makefile 的make test目标里,每次改完代码跑一遍,比手动靠谱。
性能分析上,如果实验要求统计编译时间或生成代码行数,用time ./compiler < test.c和wc -l output.asm即可。我习惯在 AST 节点里加一个node_count静态变量,每次构造递增,最后打印出来,能直观看到不同文法的树规模差异。
最后一个习惯:每完成一个阶段,用git commit打一个 tag,比如lexer-done、parser-done。编译原理实验的调试周期长,回退到上一个可用版本能省很多后悔药。希望帮到你。
本文还有配套的精品资源,点击获取