首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
编译原理实验报告写作指南:Flex+Bison构建词法语法分析到中间代码
📅 2026/10/11 16:12:06
✍️ 爱科研究院
👁 阅读 3,247
简介一份面向东北大学秦皇岛分校计算机与通信工程学院编译原理课程实验的词法分析程序设计与实现报告适合正在学习编译原理或需完成PL/0语言词法分析作业的本、专科学生参考。报告包含完整的实验目的、内容、环境、流程、总结与可运行C源码详细展示了字符分类、关键字匹配、二元组输出及“2A”等非法单词识别能帮助读者快速理解词法分析器从读取pl0.txt到生成单词串的完整过程。资源包为单个doc文档体积122KB仅含1个文件便于直接查看与编辑内容还涵盖运算符、界符判断等关键函数的设计思路。目前已有97人学习。对于初次接触编译器前端或需要实验报告模板、代码对照调试的读者这份材料兼具思路讲解与代码参考价值可为后续语法分析、语义分析学习打下基础。1. 编译原理实验报告先想明白这份报告到底在证明什么“编译原理实验报告”的标题摆在面前多数人的第一反应是贴代码词法分析贴一遍 Flex 规则语法分析贴一遍 Bison 规则然后写一句“实验完成结果符合预期”。但换个角度想阅卷老师或者面试官真正想看到的不是“你写了什么”而是“你做的编译器骨架每一步能不能拿出可验证的输出”。这也正是编译原理实验和其他课程实验最大的区别它是一整条由 Token 流、语法树、中间代码串起来的证据链任何一环断了后面的结论都站不住。这篇文章不打算空谈理论而是把从词法分析到三地址码这个最小闭环拆开讲清楚目标怎么定、框架怎么搭、数据怎么留、报告怎么写。适合正在写编译原理实验报告的学生也适合想用 FlexBison 搭一个教学用小型编译器的开发者。2. 把实验目标拆成可验收的阶段词法、语法、中间代码与运行时不管学校给的是 C--、MiniC 还是自定义的 Tiny 语言做实验的路径几乎都一样先把源代码变成 Token 流再把 Token 流变成抽象语法树接着生成三地址码最后要么解释执行、要么做目标代码生成。真正决定报告质量的关键不是每个阶段写没写而是每做完一步你手上有没有一个能拍在桌面上的输出。词法分析可以输出一条条带行列号的 Token 记录语法分析可以输出一棵可打印的 AST中间代码生成可以输出四元式风格的三地址码解释执行可以输出最终的计算结果。每一步的输入输出都有明确边界实验报告的证据链也就自然串起来了。2.1 词法分析先有一张能自检的 Token 表词法分析器最核心的产物不是“识别成功”这四个字而是一条带类型、字面值、行号和列号的 Token 流。我见过不少学生只打印 Token 类型和字面值不打印位置信息等到语法分析阶段报错时调试器只能吐出一个孤零零的line 0完全没法定位到源代码的具体位置。所以在设计词法输出格式时至少应该包含五列行号、列号、Token 类型、字面值、说明。比如下面这段输出1:1 keyword int 类型关键字 1:5 id a 变量名 1:7 assign 赋值运算符 1:9 number 1 整型字面值 1:10 semicolon ; 语句结束符不是每一条 Token 都有价值但有了这个格式语法分析阶段一旦报错你可以直接说出“错误出现在第 1 行第 5 列附近的a”而不是笼统地写“语法错误”。词法分析的另一个麻烦是关键字与标识符的匹配顺序。最常见的情况是if既匹配关键字规则又匹配标识符规则Flex 默认按最长匹配原则处理这能解决大多数冲突但像和这种前缀重叠的运算符必须把长规则写在短规则前面否则会被拆成和。如果你正在看教材比如王生原版编译原理教材里的词法状态转换图整理成规则表会非常直观标识符、整数、运算符、关键字、空白各占一行代码写起来基本不会漏状态。2.2 语法分析与 AST让程序状态能被打印和断言语法分析器最容易被误解的地方在于既要能接受合法程序也要能拒绝非法程序。很多同学只验证前者拿几个正常样例跑通就结束了但一个什么都接受的语法分析器同样能通过“正常”用例因为它在非法输入面前毫无反应。所以报告里需要两套证据一套是合法程序的 AST 输出一套是非法程序报错后的输出和恢复行为。具体到实现上AST 的构建应该独立于语法分析动作之外——Bison 的归约动作里只创建节点、建立父子关系不直接打印调试信息。打印逻辑放到单独的 dump 函数里用缩进表示树的层次Program ├── AssignStmt │ ├── Var(a) │ └── BinOp(Add) │ ├── Num(1) │ └── Num(2)这样做的原因是一旦你在归约动作里掺杂了打印逻辑AST 的结构就没办法被上层复用后续做中间代码生成时必须重写遍历逻辑。另一个容易被忽略的是每个 AST 节点都要记录源码位置。如果节点上没有行号列号三地址码生成阶段报错时也只能指着临时变量告诉用户“大约在某个地方”这不算合格的编译器行为。我在实验里通常要求struct ASTNode自带line和col字段无论是 BinOp 还是 AssignStmt创建时统一填充。2.3 中间代码生成与解释执行把“学过的理论”变成能跑的东西中间代码的选型建议优先用三地址码。它和教材里的四元式几乎一一对应每条指令最多三个操作数一个结果表达a b c这种运算非常直观解释执行时也几乎不需要额外处理。三地址码的输出格式可以定为五元组(op, arg1, arg2, result, line)其中line记录这条指令对应的源代码行号。这样做的好处是当解释执行器报“除数为零”时能直接定位到源程序里对应的一行而不是给出行号未知的虚拟指令。如果实验时间允许我强烈建议加一个简单的解释执行器让语法分析和中间代码生成的成果落到“真的能跑”而不是“看起来能跑”。输入一小段循环程序输出最终计算结果这条路径会让整个实验的完成度高一个档次。不过要注意阶段边界词法分析不碰结构语法分析不碰运算中间代码生成不碰执行。边界越清晰报告里每个阶段的引言越好写因为只需要说清楚“这个阶段接受什么、输出什么、怎么验证”。3. 用 FlexBison 搭一个最小可运行实验框架编译原理实验的技术栈常见的有三种手动递归下降、FlexBison 组合、以及 Java 平台上的同类工具。不同学校要求不一样但 FlexBison 在 Linux 和 WSL 环境下最省事词法规则和语法规则各写一个文件构建命令一目了然调试信息也比手写词法器完整。所以这一章用 FlexBison 做示范。你只需要四个文件词法规则lexer.l、语法规则parser.y、AST 实现ast.c、构建脚本Makefile。别着急一次写完先把最小路径跑通再逐步加语法。3.1 Flex 词法规则写一个能覆盖 if/else/while 的词法文件下面是一个最小可用的词法规则文件支持整数、标识符、赋值、加减乘除和关系运算符足够支撑一个包含循环和条件分支的小语言实验%{ #include parser.tab.h #include stdio.h extern int yylineno; %} digit [0-9] letter [a-zA-Z_] id {letter}({letter}|{digit})* %option noyywrap %% if { return IF; } else { return ELSE; } while { return WHILE; } int { return INT; } { return ADD; } - { return SUB; } * { return MUL; } { return LE; } { return LT; } { return ASSIGN; } {digit} { yylval.ival atoi(yytext); return NUM; } {id} { yylval.sval strdup(yytext); return ID; } [ \t\n] { /* 跳过空白 */ } . { fprintf(stderr, 非法字符: %s (行 %d)\n, yytext, yylineno); } %%%option noyywrap的作用是让 Flex 读完输入后不调用yywrap()函数这样能省去额外链接一个无用库的麻烦。%{ %}之间是 C 代码声明其中#include parser.tab.h是为了拿到 Bison 生成的 Token 定义所以编译顺序上必须先跑 Bison、再跑 Flex。{digit}规则里用yylval.ival传整数{id}规则里用strdup复制标识符字符串这些语义值会被语法分析阶段用来构建 AST。如果你需要浮点字面值记得把digit规则扩展成包含小数点的模式如果你需要字符串类型语义值的存储方式也要改成统一的联合体结构。空白规则单独处理换行符、Tab、空格全部跳过同时行号由 Bison 的%locations自动维护不需要在每条规则里手动加yylineno。3.2 Bison 语法规则与 AST 构建解决移进-归约冲突的最小方式语法文件里最关键的是规则动作和优先级声明。下面是parser.y的骨架涵盖赋值语句、while 循环和 if/else 分支%{ #include stdio.h #include ast.h extern int yylex(void); void yyerror(const char *msg); %} %union { int ival; char *sval; struct ASTNode *node; } %token sval ID %token ival NUM %token IF ELSE WHILE INT %token ADD SUB MUL ASSIGN LE LT LP RP LB RB SEMI %type node program stmt expr assign_stmt while_stmt if_stmt %left LE LT %left ADD SUB %left MUL %% program : /* empty */ | program stmt { append_stmt($2); } ; stmt : assign_stmt { $$ $1; } | while_stmt { $$ $1; } | if_stmt { $$ $1; } ;%union定义了语义值类型整数、字符串指针和 AST 节点指针。%token sval ID声明标识符的语义值是字符串%type node stmt声明语句的语义值是 AST 节点。优先级声明里%left LE LT表示关系运算符左结合且优先级最低%left MUL在最后面优先级最高。这段语法里最值得写进报告的细节是悬空 else 问题else既可以匹配最近的if也可以匹配更远的ifBison 默认移进策略会让它匹配最近的if。这是典型的移进-归约冲突你可以在文件开头加一行%expect 1声明预期一处冲突并在注释里写明这个冲突来源评阅人会认为你理解语法歧义的处理而不是碰巧没报警告。AST 的构造在归约动作里完成比如assign_stmt: ID ASSIGN expr SEMI的动作就是$$ new_assign_node($1, $3);。节点一旦创建就不再依赖语法分析器的状态后面的中间代码生成只需要递归遍历这棵树。3.3 构建命令与 Makefile三个命令跑通整个实验依赖顺序是 FlexBison 实验里最常见的坑。Bison 先生成parser.tab.h和parser.cFlex 在读取parser.tab.h之后才能生成lexer.c最后两个 C 文件一起编译链接。把依赖写清楚make就不会乱序执行all: compiler compiler: lexer.c parser.c ast.c gcc -g -o compiler lexer.c parser.c ast.c -lfl lexer.c: lexer.l parser.tab.h flex -o lexer.c lexer.l parser.c parser.tab.h: parser.y bison -d -t parser.y clean: rm -f compiler lexer.c parser.c parser.tab.h-d让 Bison 输出头文件-t开启语法调试跟踪。-lfl链接 Flex 运行时库如果老师不允许使用外部库可以去掉它但词法文件里必须有%option noyywrap并自己提供一个main函数。运行方式建议两种都试一遍./compiler test.c读文件以及echo a1; while(a3) aa1; | ./compiler读标准输入。第二种方式特别适合写进报告的“命令证据”部分因为输出和命令可以同时截图。需要说明的是在 Windows 原生环境跑 Flex 和 Bison 容易碰版本兼容问题我更推荐用 WSL包管理安装flex bison也就一条命令的事。越早把环境跑通后面调语法的时间越充裕。提示如果实验要求手写递归下降那么 Flex 部分可以保留Bison 换成你自己的语法分析函数AST 构建逻辑完全复用。工具可以换阶段划分和验证思路不变。4. 实验数据怎么收集报告才站得住用例、快照与四个避坑点很多实验报告的扣分点不在代码功能而在数据不完整。只贴一个成功样例等于告诉评阅人“我只测了一条路”。完整的实验数据应该包含正常路径、边界路径和错误恢复路径每条用例都要有输入、期望产物、实际产物。这样写出来的报告才能从“我写了代码”变成“我验证了代码”。4.1 测试用例表覆盖正常路径、边界和错误恢复下面是适合直接放进实验报告的用例表格包含一条正常路径、一条边界路径和一条错误恢复路径。期望产物不一定都是“成功”报错也是一种预期结果关键在于预期与实际是否一致编号输入摘要期望产物实际产物T1int a; a12*3;Token 流、AST、三地址码执行结果为 7与期望一致T2if (a3) aa1; else aa-1;AST 无报错执行结果符合分支逻辑与期望一致T3while(a5) aa2;循环正常终止最终 a 值为 5与期望一致T4a1;变量未声明报错第 1 行第 1 列未声明标识符 a报错位置准确T512))语法错误解析器错误恢复后不崩溃报错后继续解析后续语句这张表的要点是错误处理用例的期望产物必须写“报错且行为明确”而不是笼统的“程序出错”。比如 T4 中就应写明报错发生在哪个行列位置这要求符号表查询和错误报告机制同时工作到位T5 则要说明解析器在遇到))后如何恢复这正好验证实验里是否做了同步机制。4.2 符号表与内存释放报告里最容易被追问的点符号表是编译原理实验报告中特别容易“一问就倒”的部分。很多实现看起来能用哈希表存符号但追问“符号什么时候插入、什么时候删除、块内重名怎么处理”就答不上来。常见做法是维护一个作用域栈进入{}时压入一层退出时弹出整层查名字时从顶层向下逐层查找。这样处理块级作用域自然且便于展示。如果实验不要求做完整语义分析符号表也可以在语法分析阶段同步收集声明再用一个单独遍历 AST 的过程做校验两边职责分开。报告里建议画一张很小的作用域快照程序进入while块之前符号表只有a进入之后多了一个b退出之后b消失。内存释放是另一个容易被评阅人抓的细节。一个功能正确但到处泄漏的程序算不上合格的实验成果。验证方法很简单在 Valgrind 下跑完整个用例集贴出definitely lost: 0 bytes的运行结果。实现上要约定清楚所有权规则——词法分析里strdup出来的字符串归 AST 节点所有节点销毁时统一释放Bison 动作里临时产生的中间节点如果已经挂到树上就不要在别处二次释放。这块代码不复杂但需要提前规划。有的同学为了省事干脆不释放等到实验验收时一打开 Valgrind报告的说服力直接就碎了。4.3 避坑记录四个高频踩坑点下面四条是我在不同实验里反复见到的问题按“现象→原因→解决”列出来每一条都有对应的报告写法。坑一编译通过一运行就 Segmentation fault。现象是程序对合法输入直接崩溃。原因多半是yylval.sval指向的字符串内存没有分配或者语义值所有权不明确两个模块都去释放同一块内存。解决方法是统一规则词法分析负责分配标识符字符串AST 节点持有指针销毁节点时释放其他模块只读不写。坑二make报错parser.tab.h: No such file or directory。现象是 flex 找不到 Bison 生成的头文件。原因很简单构建顺序错了flex 先于 bison 执行。解决方法是把lexer.c这条规则依赖上parser.tab.h同时给 Bison 规则加上-d选项让make自己去排依赖顺序。坑三输入a12*3;执行结果一直是 7改成三地址码再看发现加法顺序颠倒。现象是程序能跑但结果不符合优先级。原因是 AST 递归生成三地址码时遍历顺序和中间代码临时变量的产生顺序没有保持一致。解决方案是统一递归顺序先左子树、再右子树、最后生成结果。这类问题在报告里写清“修改前是 6修改后是 7”反而是一个加分点证明你确实调试过。坑四错误恢复后继续解析却报一堆假错误。现象是遇到一个非法字符后面每条语句都跟着报错。原因是语法分析器在错误状态中继续处理输入没有做同步处理。解决方式是在语句规则里加一条错误恢复规则比如error SEMI { yyerrok; }让解析器跳到分号之后再恢复正常。这里可以顺带说明yyerrok的作用是重置错误标志以及YYRECOVERING()宏可以用来判断当前是否仍在恢复状态。把这一点写清楚说明你不只会写动作还理解语法错误处理的机制。5. 把实验报告写成能读懂的工程文档给分点、证据链与我的习惯实验报告的最终形态不是“代码 截图”而是让评阅人能在几分钟内确认“这个人确实装好工具链、跑通流程、理解了每一步”。我组织报告时固定用四个元素实验目标、技术路线、证据快照、分析与结论。其中证据快照是最容易被忽视却最值钱的部分。所谓快照不是把终端输出整个复制粘贴一遍而是每条命令下面跟一句说明告诉读者“这条输出证明了什么”。比如 Token 流贴出来后跟着写“可以看到没有被拆成和说明最长匹配规则生效”AST 贴出来后写“a12;对应的树中乘法节点先于加法节点生成符合优先级”。给分点通常藏在“你能不能解释每个输出”上。词法分析输出 Token 流之后如果能指出某条 Token 为什么是某种类型报错回报为什么定位准确这段时间花的就值。三地址码输出之后如果能从中间代码反推回源程序语句实验的完整性立刻上一个台阶。验证方法我建议统一跑固定用例集把每次输出的输出重定向到带时间戳的文件里比如snapshot_20250411_2030.txt。这个习惯能帮你回滚调试也能在报告附录里放一张改动记录写清楚第几个版本修复了哪类错误。我还习惯在报告里保留一条“错误恢复”用例的输出并明确写它是一开始失败的。很多学生不愿意展示失败觉得会扣分实际上展示你会改 bug比展示“一次成功”更有说服力。回到这个标题——编译原理实验报告本质上是你对“编译器如何构建”这件事的复盘第一步能不能走通不重要重要的是你有没有证据说明走到哪一步、为什么停在那里、最后怎么绕过去的。希望这个流程能帮你少走几个弯路也祝实验顺利。本文还有配套的精品资源点击获取
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/11 16:12:06
LeetCode 1390四因数:因数个数公式与质因数分解优化指南
2026/10/11 16:07:06
Darknet YOLOv3烟雾检测:2000张数据集训练与部署实践指南
2026/10/11 16:07:06
Docker容器内Permission denied且无sudo?一篇讲透排查与根治
2026/10/11 18:27:17
基于VGG的自然灾害图像分类:从迁移学习到部署的完整实战指南
2026/10/11 18:27:17
PnP位姿测量实战:从算法选型到避坑的完整指南
2026/10/11 18:27:17
电视的声音重要吗?索尼电视7系二代与海信E8S的听感各有侧重
2026/10/11 18:27:17
大模型验证码识别实战:从传统OCR到视觉大模型的完整方案
2026/10/11 18:27:17
自托管家庭ERP实战:Grocy库存管理与外部访问全攻略
2026/10/11 18:22:17
C# WinForms工控界面高级设计:双缓冲、自定义控件与跨线程更新
2026/10/11 0:00:10
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/11 0:00:10
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/11 0:00:10
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/11 0:00:10
流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南
2026/10/11 0:00:10
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别
2026/10/11 0:00:10
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容
2026/10/10 3:41:56
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/10 3:41:54
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/9 11:36:17
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)