首页 > 仰望星空 > 自制 MC90 编译系统开发日记
2019
10-24

自制 MC90 编译系统开发日记

写日记的原因

从下决心要自己从零开始写一个 C 语言编译器开始到现在,已经过了一个多月时间了。其实,我作为一名身负着学业的大三普通学生,基本上没有太多的时间完完全全地投入到自制编译器的开发过程中,因为只能在闲暇时构思设计方案以及实现代码,所以开发进展很缓慢,开发周期会比较长。这样的话,编写详细的代码注释和开发手记就很重要,注释能提高代码的可读性,以便于今后长期的开发工作能够可持续地进行下去,而开发手记更像是对自己在开发过程中的思路、遇到的问题进行阶段性的归纳总结。尤其像编译系统这种代码量相比原来大得多的项目,开发手记就显得更加重要。

MC90 编译系统

这个编译器的名字叫做 MC90,MC90 意思就是 “迷你的、兼容 C90 标准的编译器” (Mini C 90)。说到这儿,其实很多朋友可能会建议,可以采用更加个性的、霸气的名字来命名这个 C 语言编译器。我觉得没那个必要,因为我自认为自己水平有限,写出来的编译器配不上某些炫酷霸气的名字,而且我想编写这个编译器的初衷其实是想提高编程能力。比起炫技什么的,开发过程中的乐趣反而更加地吸引我。这份乐趣同样也是支持我学习并实践编程技术的持续动力,它有时可以使我忘记生活中的烦恼不开心的事情,在编程中获得喜悦感和成就感。而 MC90 这朴实无华的名字正合我心意。

2019年10月25日

既然要设计 C 语言编译器,那么预处理器的设计是首先需要考量的。预处理也正是 C 语言编译系统编译过程的第一步。

到目前为止,mc90 预处理器已经完成了一部分了,主要有三个模块:

行扫描器模块字符扫描器模块预处理单词扫描器模块

行扫描器模块主要由 CombinedLine 类以及 LineScanner 类组成。CombinedLine 是针对 C 语言源程序文本的、以行划分的基本数据结构,一个 CombinedLine 对象包含一个或多个 SingleLine 底层对象(封装采用 std::vector<SingleLine> 实现)。SingleLine 是 std::wstring 类的别名,CombinedLine 描述一行组合行的信息,组合行由一个或多个单行组成,这些单行除最后一行以外结尾都以行延续符 ‘\’ 结尾。

LineScanner 类提供从输入流中扫描并读取组合行的功能,它维护一个可回溯的线性容器,用于存放实际上已经被读取出的、连续的 CombinedLine 对象,其内部采用一个指示器来对底层线性结构进行抽象,并提供组合行扫描过程的回溯和前进的功能。无论如何,LineScanner 类都将保证流中的组合行对象的可回溯性和完整性。

字符扫描器模块主要由 BasicCharacterScanner 类以及 PracticalCharacterScanner 类组成。BasicCharacterScanner 以一个 CombinedLine 对象作为输入,能够扫描并读取其中的字符。这个读取过程被抽象成线性的,并且 BasicCharacterScanner 内部基于 CombinedLine 对象提供的可回溯机制,同样提供了字符的回溯和前进功能。PracticalCharacterScanner 是对 BasicCharacterScanner 的更高级的封装,PracticalCharacterScanner 能有效地过滤掉 CombinedLine 中存在的单行注释和多行注释,并返回除此之外对预处理器和编译器有实际意义的字符。

这些字符最终被封装成 CharacterInfo 数据结构,用于返回字符的值,还有字符所处的行、列的位置信息,便于调用者进行错误定位和汇报。

预处理单词扫描器模块主要由 PreToken 类和 PreTokenScanner 类组成。PreTokenScanner 内部维护一个 PracticalCharacterScanner 对象,通过对 CombinedLine 输入进行字符读取,使用一套特定的有限状态机算法实现由字符到单词的构造功能,这套算法刚好利用了 PracticalCharacterScanner 对象的可回溯机制。扫描器内部也维护了存放 PreToken 对象的线性容器。同样地,PreTokenScanner 提供了可回溯机制,使得上层模块能够在单词流中回溯或前进,这为语法分析提供了基本的数据结构和算法。

PreToken 对象是预处理单词对象,它主要描述了单词的类型和值(单词内容)以及单词的起始行号和起始列号,这些单词最终会被 PreParser 拿去用于构造语法分析树,当然,PreParser 也是通过 PreTokenScanner 来抽取这些 PreToken 对象的。

2019年10月30日

在开发过程中,我发现如果 PracticalCharacterScanner 对每一个以 CombinedLine 对象来做底层封装的 BasicCharacterScanner 进行消除注释处理,那么这个工作过程将变得十分繁琐。

首先,CombinedLine 由 LineScanner 从 std::ifstream 对象中产生 (这里我定义了别名 InputFileStream,下文都用这个别名来表示字符文件输入流),这时每一个 CombinedLine 对象都是完整的。BasicCharacterScanner 以 CombinedLine 作为输入进行字符读取,而 PracticalCharacterScanner 通过对 BasicCharacterScanner 的封装,调用其读取字符的同时也消除注释。那么,问题就出现了。

注释有行注释和块注释两种,这两种注释都可以以跨越多行的形式出现,块注释自然不用说,行注释也能通过换行之前的行延续符(反斜线“\”)实现跨越一行或者多行(除最后一行之外都使用行延续符)。

如果注释跨越多行,那么 PracticalCharacterScanner 对某一个 CombinedLine 对象的处理显然是不够的,对于跨越多行的注释消除操作,需要一次对多个 CombinedLine 对象进行处理,这样设计就会变得十分复杂。而且,LineScanner 在对 InputFileStream 进行读取并生成 CombinedLine 对象时,CombinedLine 的类型就已经被确定下来(预处理器指令行或者普通程序行),这将直接影响预处理器对此行的处理操作,预处理器只会处理预处理器指令行。那么,如果遇到某种多行注释,刚好延续到某行的预处理器指令之前,那么首先 LineScanner 是不会将此行作为预处理器指令行的,会作为普通程序行进行处理 (LineScanner 会判断一行的首个字符除空格和制表符是否是“#”号,若是,则确定是预处理器指令行,显然,这种情况下,首个非空白的字符不可能是“#”)。

除此之外,延续行符运用在单行注释上的情形,与上述情形类似。如果行注释延续到某一个注释内容的下一行,假设下一行的开头是 “#” 号 (哈哈,当然这种情况很少),按常规情况,这以 “#” 开头的一行也应该被当作注释,可惜,LineScanner “并不知晓” 它是注释行,而且会把此行当作预处理器指令行。

其实,说那么多,设计过程中的问题其实就是“剔除注释”和“确定组合行类型”的先后顺序的问题

最近我已经想到了解决方法,其实很简单,让 BasicCharacterScanner 直接对 InputFileStream 进行字符读取,PracticalCharacterScanner 基于 BasicCharacterScanner 从 InputFileStream 进行字符读取的同时,使用有限状态机算法,消除行注释和块注释,用空白字符去一一替换原有在注释内容中的字符,除了换行符之外。最后 LineScanner 将负责调用 PracticalCharacterScanner,将根据剔除了注释的字符,处理并生成 CombinedLine 对象,这时结果将是正确的。


打赏 赞(7)
微信
支付宝
微信二维码图片

微信扫描二维码打赏

支付宝二维码图片

支付宝扫描二维码打赏

最后编辑:
作者:Yenyu
Yenyu
编程爱好者

自制 MC90 编译系统开发日记》有 1 条评论

  1. 头像 一位不愿透露姓名的inrcl 说:

留下一个回复

你的email不会被公开。