简介:面向编译原理课程实践的中国海洋大学2020年春季实验代码合集,涵盖词法分析、语法分析、语义分析、中间代码生成、代码优化、目标代码生成、错误处理及编译器综合等8个实验,并附实验要求文档,适合正在学习编译器构造或备战课程实验的本科生参考。压缩包共74个文件,包含C源码、lex/flex的.l文件、yacc/bison的.y文件、头文件与makefile构建脚本,另有编译生成的可执行文件和示例输入,整体约774KB,目录按实验编号组织,便于定位对应阶段的实现。资源目前已有4408人学习浏览,可见其对本课程学生有实际帮助。通过这套代码,读者既能对照实验要求逐项理解词法规则、文法设计与语义检查的具体写法,也能从lex.yy.c、parser.tab.c等生成文件和测试用例中掌握Flex/Bison工具链的调用方式,进而串联起从源程序到中间代码再到目标代码的完整编译流程,值得配合教材边读边练。 先说结论:编译原理这门课,上课听懂和实验跑通是两回事。我在OUC把这门课的全部实验从头到尾吃透之后,最大的感受就是——课本里的正则表达式、FIRST集、LR分析表,只有亲手写进代码里,再对着报错信息一次次修正,才算真正变成自己的东西。
这篇文章就围绕OUC编译原理全部实验的完整链路,从整体设计思路、工具选型、核心细节、常见排查这几个方面,把每个环节的实操要点和踩坑记录整理出来。无论你是刚开始接触这门课、正在为第一个词法分析实验发愁,还是已经把语法分析写得快崩溃,这篇文章都能给你一份可直接参考的路线图。
1. 实验全貌与整体思路拆解
1.1 这套实验到底在做什么
OUC编译原理课程实验的核心,是让你亲手实现一个“迷你编译器”的各个阶段。别被“编译器”三个字吓到,把它拆开来看就清晰了:编译过程就像一条流水线,源代码先进来,经过词法分析切成一个个单词,再经过语法分析按规则组合成句子,之后进行语义分析确认意思没有矛盾,最后翻译成目标代码或中间代码。
整套实验一般分为五个阶段:词法分析、语法分析、语义分析与符号表管理、中间代码生成、目标代码生成(部分学期还会加上简单的代码优化)。每个阶段对应一个实验,环环相扣。我在做的时候感触最深的一点是:前一个阶段的输出就是后一个阶段的输入,所以越早把接口设计清楚,后面越省力。
1.2 实验之间的依赖关系与进度安排
这里有一个很容易踩的坑:很多同学把每个实验当成独立任务来做,结果做到第三个实验发现前面设计的Token结构完全不够用,符号表也没预留扩展位,只能推倒重来。
正确的打开方式是:先通读全部实验要求,把所有阶段的输入输出接口统一设计好。比如词法分析输出的Token类型,不仅要覆盖语法分析需要的运算符、关键字,还要提前考虑语义分析阶段的类型信息字段;符号表的构建虽然属于语义分析阶段,但它的键值设计最好在词法分析时就确定下来。
我当时的时间安排是:词法分析一周,语法分析两周,语义分析一周,中间代码生成一周,目标代码生成一周,最后留出三天做整体联调。语法分析花的时间最长,因为几乎所有经典报错都集中在这个阶段。建议你也把时间重心放在这里。
2. 环境与工具选型:别在起跑线上内耗
2.1 开发语言怎么选
OUC的编译原理实验没有强制指定语言,但绝大多数人会选择Java或C++。我自己用的是Java,原因有三个:一是面向对象的结构很适合分层实现编译器的各个阶段,词法分析器、语法分析器、符号表各开一个类,接口清晰;二是调试工具链成熟,Eclipse或IDEA里断点调试多线程程序很方便(后面会讲为什么多线程相关);三是字符串处理能力比C++舒服很多,做正则匹配和处理源码文本时省心。
如果你之前主攻C++,完全没问题,编译原理实验对语言本身的要求不高,关键是逻辑正确。只是C++里字符串和容器要小心越界和内存问题,调试成本略高。不要在这上面花太多时间纠结,选自己最熟的语言就是最优解。
2.2 手写还是用工具生成
这是实验开始前一定会纠结的问题:词法分析可以用flex/jlex自动生成,语法分析可以用yacc/bison/JavaCC,直接用工具是不是更快?
我的建议是:老老实实手写。理由很简单——实验的考核点在于你是否理解DFA、LL(1)这些原理,而工具自动生成的代码会把这些过程封装得严严实实,你只是在调用一个黑盒。一旦答辩时老师问你状态转换图怎么设计的、FIRST集怎么算的,答不上来就尴尬了。手写词法分析器和递归下降语法分析器,代码量并不大,核心逻辑也就几百行,完全在可控范围内。
注意:有些版本的实验要求明确规定“不得使用生成工具”,交上去会直接扣分。动手前先确认清楚。
2.3 第三方库与配置要点
我的Java实验只用了JDK自带的功能,没有引入任何第三方依赖。有些资料会推荐用ANTLR来辅助验证语法树结构,但那只适合做参考对照,不能作为实验提交内容。IDE方面,我建议直接用IDEA社区版,免费且Java支持完善。
如果你和我一样是Java党,有一个小建议:把每个阶段的输出结果(Token流、语法树结构、四元式序列)单独打印成文本文件放好,这是后面联调时的宝贵依据。
3. 核心实验实现细节与避坑实录
3.1 词法分析:状态转换图与种别码设计
词法分析实验是整个编译器流水线的第一站,目标非常明确:把源代码字符串识别成有意义的Token序列。它的理论基础是有限自动机DFA,这可能是整个编译原理课程里最“看得见摸得着”的部分。
我做的第一件事是设计种别码表。这个表定义了每种Token的数字编号,例如:
| 种别 | 种别码 | 说明 |
|---|---|---|
| ID | 1 | 标识符 |
| INT | 2 | 整型常量 |
| PLUS | 3 | + |
| MINUS | 4 | - |
| STAR | 5 | * |
| ASSIGN | 6 | = |
| SEMI | 7 | ; |
| LPAREN | 8 | ( |
| RPAREN | 9 | ) |
| KEYWORD | 10 | if/else/while等 |
关键点在于:标识符和关键字要分开处理。常规做法是先把所有关键字存进一个HashSet,识别完一个以字母开头的单词后,先查这个集合,如果命中就是关键字,否则就是标识符。要特别注意大小写,C系语言通常区分大小写,Pascal不区分,看实验要求。
词法分析器的核心代码逻辑其实不复杂,就是一个大循环加状态判定:
public Token nextToken() { skipWhitespace(); if (Character.isLetter(ch)) { StringBuilder sb = new StringBuilder(); while (Character.isLetterOrDigit(ch)) { sb.append(ch); advance(); } String word = sb.toString(); if (keywords.contains(word)) { return new Token(TokenType.KEYWORD, word); } return new Token(TokenType.ID, word); } // 其他符号处理... }这里最容易踩的坑是文件结束符EOF的处理。我第一版代码里没有显式处理EOF,导致最后多识别出一个空Token,后面的语法分析在遍历Token流时越界。正确做法是定义一个专门的EOF种别码,遇到输入结束就返回这个Token,语法分析阶段看到EOF就意味着程序结束,逻辑干净多了。
3.2 语法分析:递归下降与LL(1)分析表的选择
语法分析是整套实验的分水岭。OUC的实验中,这部分通常有两种实现路线:递归下降分析法或构造LL(1)分析表驱动的预测分析器。我选择的是递归下降,因为它写得直观,出现错误也好调试。
递归下降的实质,就是为文法中的每个非终结符写一个函数,函数内部按照产生式规则去匹配终结符和非终结符。你可以把它理解为“套娃”:非终结符的函数内部会调用其他非终结符的函数,一层层展开,直到匹配到真正的单词。
这个过程最痛苦的问题就是左递归。像表达式文法expr -> expr + term直接写成递归下降就是死循环,函数还没读完输入就又调用自己了。解决办法是改写成右递归形式:
expr -> term expr' expr' -> + term expr' | ε我当时卡在这个点上整整一个下午,因为没想明白为什么要改写、改写后优先级怎么处理。后来弄清楚了:左递归改成右递归后,加法的左结合性质要通过返回值的累加来实现。递归函数不再是一进来就递归,而是先压入栈中慢慢展开,计算时机要反过来。理解了这个,表达式解析的代码就能写得非常顺畅。
优先级方面,递归下降里靠“层级”来体现:表达式层调用项层,项层调用因子层,因子层才是数字和括号。解析优先级不是靠判断条件,而是靠函数的嵌套调用来自然体现的。
3.3 语义分析与符号表:作用域和类型的双重考验
语法分析过关之后,语义分析直接考验你对作用域和类型的理解。我实现符号表用的是栈式结构——维护一个Map的栈,每进入一个块就压入新的一层,退出就弹出。查找变量时从栈顶往下找,遇到同名的就优先返回栈顶的。
这个设计看起来简单,实际用起来效果很好。处理函数作用域时,函数参数也往符号表里塞;处理函数内部的局部变量时,压入新层;函数结束再弹出。有一个容易忽略的地方:同一个函数内部嵌套块里声明了同名变量,这在内层块的顶层变量查找时会掩盖外层的同名变量,这是C语言等常见语言的规则,符号表的栈式查找天然就支持了这一点。
类型检查是语义分析的重头戏。我检查了赋值两侧类型是否匹配(整型赋给浮点型要允许隐式转换,反过来则要告警或报错)、运算操作数类型是否一致、函数调用实参和形参个数及类型是否一致。
这里我踩过一个印象深刻的坑:早期实验定义的Token只有词法信息,没有类型字段。做语义分析时发现,类型信息必须在词法分析阶段就挂在Token上(至少对常量来说),否则一个整型常量和一个字符串常量在后续处理时傻傻分不清。这个教训直接导致了我在做实验三时回头重构了第一版词法分析的Token定义。
3.4 中间代码生成与目标代码:四元式与栈式虚拟机的完美配合
中间代码生成我采用的是四元式(op, arg1, arg2, result),比如+ a b t1表示 t1 = a + b。四元式的优点在于结构统一,后面生成目标代码就是一个模式匹配的问题。
比较麻烦的是控制流语句的四元式生成,尤其是条件跳转。if语句需要“回填”——生成跳转指令时还不知道目标地址,只能先留空,等后面解析完else分支再回头填上目标标签。我实现了一个临时标签队列,通过维护“需要回填的四元式序号列表”来实现。
目标代码生成阶段,OUC的常规要求是将四元式翻译成栈式虚拟机的指令,因为真实汇编涉及寄存器分配就太复杂了。栈式虚拟机的指令格式一般是简单的一条指令加操作数,比如:
ILOAD a ILOAD b IADD STORE t1翻译的核心思路就是:把四元式的arg1和arg2分别压栈,然后用对应运算符指令弹出两个数做运算,结果压栈,最后用STORE弹出栈顶值存入arg2。这个过程逻辑很机械,但这正是考察你是否真正理解操作数寻址和运算流程的地方——每个变量、临时变量在栈中的位置靠偏移量来定位,并全部记录在一个映射表里统一管理。
提示:中间代码和最终代码的对应关系,建议你画一张对照表放在手边。调试一旦报错,第一件事就是检查四元式到目标代码的映射是否吻合,而不是去猜目标代码哪里出了问题。
4. 常见问题与排查技巧实录
4.1 高频报错与对应解法
编译原理实验调试起来最烦人的地方在于:报错信息往往不是你真正的错误点。尤其是语法分析阶段,一个括号没配对,后面几十个Token全乱套。这里把我在实验中遇到的高频问题整理成一张表,希望对你有直接帮助:
| 现象 | 根本原因 | 排查方法 |
|---|---|---|
| 词法分析多出空Token | 没处理EOF边界 | 调试时打印Token总数和最后一个Token类型 |
| 递归下降卡死 | 左递归未消除或Token未推进 | 检查token索引是否每次匹配后自增 |
| 语法分析栈溢出 | 递归无终止条件 | 打印递归深度,看输入是否在减少 |
| 符号表查到未定义变量 | 作用域规则没弄对 | 检查块退出时是否弹出栈层 |
| 语义分析类型错误堆叠 | 类型信息未在Token中保留 | 重构Token定义,增加类型字段 |
| 四元式回填错乱 | 跳转目标未正确绑定 | 打印所有label和待回填列表 |
| 目标代码栈不平衡 | 运算指令压栈弹栈个数不符 | 每条指令前后打印栈深度 |
4.2 测试数据与回归策略
写测试数据是很多人忽略但极其关键的一环。我给编译器的每个阶段都准备了三类测试用例:正常用例、边界用例和错误用例。正常用例确保核心功能工作;边界用例包括空程序、多个连续运算符、大数字、深层嵌套括号;错误用例则用来验证报错机制是否正常(比如缺少分号、变量未声明、类型不匹配)。
我在做语法分析时用了一个小技巧:把Token流逐行打印出来,对照语法规则一条条核对。语法树结构比较复杂,肉眼看不出来,我就专门写了一个打印语法树的辅助函数,用缩进直观展示树的层级关系。这个辅助函数在后来的语义分析和中间代码生成阶段帮了大忙。
联调时最重要的一点:一定要保证词法分析输出的Token流和语法分析实现的Token类型完全一致。很多同学前面用网上找的示例代码,Token类型定义和后面自己的不一致,结果一联调全是类型转换错误。建议尽早做一次“端到端”测试——拿一个最简单的含变量声明、赋值、运算、判断、循环的完整小程序,从词法分析一路走到目标代码生成,保证整条链路跑通。
4.3 一个老学长的时间管理心得
最后分享一个我自己的习惯:每个实验动手写代码前,先用纸笔把数据结构和核心流程画出来。词法分析画状态转换图,语法分析画产生式和递归关系,语义分析画符号表的结构,中间代码生成画四元式的图形表示。这习惯看起来很“土”,但真的能减少返工。
还有一个丈量实验质量的实用标准:上机测试时,尝试自己修改源代码里的一处语法错误,看看编译器能否准确报错并指出大致位置。如果你的编译器只能报“error”不能定位到行号,说明Token的位置信息没有维护好。很多同学实验做完不注意这个,到答辩时才暴露缺陷,那时已经开始倒扣分了。
编译原理的实验,本质上是在训练你把一个复杂问题拆解成一系列可管理的子问题。每一步都不算特别难,但它们串联在一起时,那种“字——词——句——义——码”的贯穿感,会让你对整个计算机系统怎么工作有一种前所未有的通透理解。希望这些踩坑记录,能让你这条路走得顺一点。
本文还有配套的精品资源,点击获取