ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

电子科技大学编译原理实验代码:从词法分析到代码生成的完整复现路径

电子科技大学编译原理实验代码:从词法分析到代码生成的完整复现路径 简介这份资源是电子科技大学编译原理课程的实验代码合集面向正在学习编译原理、需要动手实现词法分析与语法分析的高校学生及自学者。内容围绕编译器前端核心模块展开包含词法分析器与语法分析器的完整实现涉及正则表达式、有限状态自动机、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给一元负号单独指定优先级否则-34会被解析成-(34)。%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_mapstd::string, Symbol配合作用域栈是最省事的方案。每进入一个块{}压入新 map退出时弹出。查找时从栈顶往下遍历找到第一个匹配即返回。struct Symbol { std::string name; std::string type; // int 或 float int scope_level; }; class SymbolTable { std::vectorstd::unordered_mapstd::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 CFlex 不加 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或者封装成单例类。测试时用ab*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], stdinopen(src), capture_outputTrue, textTrue) with open(exp) as ef: expected ef.read() if result.stdout.strip() ! expected.strip(): print(fFAIL: {f}) print(Got:\n, result.stdout) print(Expected:\n, expected) fail 1 else: print(fPASS: {f}) sys.exit(1 if fail else 0)参数说明capture_outputTrue捕获 stdout 和 stderrtextTrue返回字符串而非字节。strip()忽略行尾空白差异。这个脚本能集成到 Makefile 的make test目标里每次改完代码跑一遍比手动靠谱。性能分析上如果实验要求统计编译时间或生成代码行数用time ./compiler test.c和wc -l output.asm即可。我习惯在 AST 节点里加一个node_count静态变量每次构造递增最后打印出来能直观看到不同文法的树规模差异。最后一个习惯每完成一个阶段用git commit打一个 tag比如lexer-done、parser-done。编译原理实验的调试周期长回退到上一个可用版本能省很多后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表