(一)编译程序总体结构
ZhouPascalCompiler是一个简化版的的Pacal编译器,由词法分析器、语法分析器、语义分析与中间代码生成器以及贯穿于前三个阶段的符号表管理器和出错处理器组成。它的工作流程为:输入源程序code.txt,词法分析分析出其中的单词符号,如果在分析过程中未出现词法错误,则生成中间文件lex.txt,否则向屏幕打印错误类型提示并定位出错位置,程序终止。这样设计的好处在于先进行比较简单的词法分析,避免了不必要的计算开销,节省了计算机资源。因为如果设计成以语法分析器为核心的结构,则在分析过程中一旦发现某个词法错误,那么之前所有的语法分析和翻译动作都白做了。若词法分析成功生成lex.txt,语法分析器进行语法分析工作:需要一个单词时,就从lex.txt读出一行,根据预测分析表和状态栈、符号栈的栈顶符号决定将此单词压栈/规约,若规约成功,则输出“accept”;否则,输出“error”和错误原因。采用语法制导翻译的方式,在语法分析的同时进行语义分析和中间代码生成,可翻译的语句类型有:含嵌套过程的声明语句、含数组的赋值语句、布尔表达式、if/while/for/repeat、过程调用语句、输入输出语句。符号表管理器用于管理程序中的各种标识符、过程名、过程嵌套关系等。出错处理对器程序不符合词法或语法要求的地方进行定位并提示出错信息。
该编译程序的源代码在https://github.com/zhouliyan/PascalCompiler
这学期在《编译原理》这门课的实验中收获了很多。最重要的是学习了一种系统设计的思想,一种对全局把握的能力。系统是一个很庞大的工程,它里面包含很多模块,模块之间相互联系,虽然尽量使用“高内聚,低耦合”的设计思想,但系统越大,不可避免地会提高模块之间的耦合程度,这时就需要设计者既有一种纵观全局的开阔眼界,又有对每一个细节的精准把握。在编写这个编译器的过程中我的体会是:代码越多,调试越困难。因为代码越多,程序一旦编写错了,定位出错位置就比较难了。比如我在编写语法分析器的过程中发现有一个错误如何调试都不对,后来发现不是语法分析的代码有错,而是之前编写的词法分析器有一个隐性的错误当时测试时没有发现,这让我感到其实程序测试也是很重要的一项的工作,尤其是在比较大型的系统编写过程中,需要对之前编写的代码进行充分的测试,保证在开始新工作之前以前的代码都是正确的。另一个让我印象深刻的错误是在编写符号表的过程中,是一个很低级的错误,但却花费了我好几天也没调试出来,就是声明一个指针int *pint;那么在访问过程中如pint+1,这个1的单位不是字节,而是这个pint类型的size,在这里相当于加了4个字节。这个错误在于我对C语言的掌握不够,以前的知识模糊。调试这个错误的过程中IDE提示的出错位置不是在这,而是在另一个malloc申请内存的语句处,这是因为以前错误对不该访问的地址访问、赋值,导致程序在申请堆上空间时出错。
C语言的指针很强大,但用不好也会导致灾难。我觉得这正是这门语言的迷人之处,它和别的语言不同,程序员对每个变量都必须清楚地知道它是从哪来的,将要到哪去,什么时候消失。换句话说,程序员对细节必须完全清楚。比如我,如果我聚精会神地编一上午Python代码,几乎一运行就能通过,结果正确。而聚精会神地编一上午C代码,调试可能还是会有点小问题需要修改。
这门课的另一个好处是让我复习了以前学过的一些有用的数据结构,比如栈、链表、队列等。而且我也收获到了如何创建自己的数据结构,并且让它更高效的经验。
还有一个很大的收获就是学习到一种“折中”的思想,这在系统设计过程中尤为重要。有时候我们没法达到鱼与熊掌兼得,效率与空间兼得,这是就需要衡量一下什么对我们来说更重要。有时候需要牺牲空间换取效率,在另一些时候则反之,需要具体情况具体分析。比如我在设计符号栈中的数据结构token的时候,因为这个符号可能是整型、实型、数组型等等,不同的类型有不同的属性,也就是需要不同的域,可以设置一个拓展的指针域存放该节点的属性们,也可以直接将这些属性当成节点的域,每个节点拥有的域都是相同的,对应不同类型的token向不同域存属性即可,其他域没有用。前面一种方案看起来可以大大节省节省空间,但我选择了后者。这样做的原因是:减少了不同类型token使用指针寻址,查找的时间,提高了效率。而且后来我改进了这种方案,即将token从符号栈中弹出的同时销毁token所占的空间,这样非常好地解决了空间问题,token占的多余空间几乎没有;另外在程序编写过程中,我发现token的一些闲置域还可以放其他的东西,更加减少了浪费。
在这门课中我完成了编译器的词法分析、语法分析、中间代码生成模块,符号表管理和出错处理还需要增强,在以后有时间的时候我会继续做下去,期望自己能完成剩余的中间代码优化、目标代码生成部分,完善符号表管理模块,增强出错处理功能。如果将来有一天能把这些完成了,我就能真正和别人说我做了一个pascal编译器了。虽然这门课结束了,但我探索编译之路才刚刚开始。