跟vczh看实例学编译原理——二:实现Tinymoe的词法分析

文章中引用的代码均来自https://github.com/vczh/tinymoehtml

 

实现Tinymoe的第一步天然是一个词法分析器。词法分析其所做的事情很简单,就是把一份代码分割成若干个token,记录下他们所在文件的位置,以及丢掉没必要要的信息。可是Tinymoe是一个按行分割的语言,天然token列表也就是二维的,第一维是行,第二维是每一行的token。在继续讲词法分析器以前,先看看Tinymoe包含多少token: git

  • 符号:(、)、,、:、&、+、-、*、/、\、%、<、>、<=、>=、=、<>
  • 关键字:module、using、phrase、sentence、block、symbol、type、cps、category、expression、argument、assignable、list、end、and、or、not
  • 数字:12三、456.789
  • 字符串:"abc\r\ndef"
  • 标识符:tinymoe
  • 注释:-- this is a comment

 

Tinymoe对于token有一个特殊的规定,就是字符串和注释都是单行的。所以若是一个字符串在没有结束以前就遇到了换行,那么这种写法定义为你遇到了一个没有右双引号的字符串,须要报个错,而后下一行就不是这个字符串的内容了。 程序员

 

一个词法分析器所须要作的事情,就是把一个字符串分解成描述此法的数据结构。既然上面已经说到Tinymoe的token列表是二维的,所以数据结构确定会体现这个观点。Tinymoe的词法分析器代码能够在这里找到:https://github.com/vczh/tinymoe/blob/master/Development/Source/Compiler/TinymoeLexicalAnalyzer.hgithub

 

首先是token: 正则表达式

CodeTokenType是一个枚举类型,标记一个token的类型。这个类型比较细化,每个关键字有本身的类型,每个符号也有本身的类型,剩下的按种类来分。咱们能够看到token须要记录的最关键的东西只有三个:类型、内容和代码位置。在token记录代码位置是十分重要的,正确地记录代码位置可让你可以报告带位置的错误、从语法树的节点还原成代码位置、甚至在调试的时候能够把指令也换成位置。 express

 

这里须要提到的是,string_t是一个typedef,具体的声明能够在这里看到:https://github.com/vczh/tinymoe/blob/master/Development/Source/TinymoeSTL.h。Tinymoe是彻底由标准的C++11和STL写成的,可是为了适应不一样状况的须要,Tinymoe分为依赖code page的版本和Unicode版本。若是编译Tinymoe代码的时候声明了全局的宏UNICODE_TINYMOE的话,那Tinymoe全部的字符处理将使用wchar_t,不然使用char。char的类型和Tinymoe编译器在运行的时候操做系统当前的code page是绑定的。因此这里会有相似string_t啊、ifstream_t啊、char_t等类型,会在不一样的编译选项的影响下指向不一样的STL类型或者原生的C++类型。github上的VC++2013工程使用的是wchar_t的版本,因此string_t就是std::wstring。 api

 

Tinymoe的词法分析器除了token的类型之外,确定还须要定义整个文件结构在词法分析后的结果: 数据结构

这个数据结构体现了"Tinymoe的token列表是二维的"的这个观点。一个文件会被词法分析器处理成一个shared_ptr<CodeFIle>对象,CodeFile::lines记录了全部非空的行,CodeLine::tokens记录了该行的全部token。 函数

 

如今让咱们来看词法分析的具体过程。关于如何从正则表达式构造词法分析器能够在这里(http://www.cppblog.com/vczh/archive/2008/05/22/50763.html)看到,不过咱们今天要讲一讲如何人肉构造词法分析器。方法实际上是同样的,首先人肉构造状态机,而后把用状态机分析输入的字符串的代码抄过来就能够了。可是不多有人会解耦得那么开,由于这样写很容易看不懂,其次有可能会遇到一些极端状况是你没法用纯粹的正则表达式来分词的,譬如说C++的raw string literal:R"tinymoe(这里的字符串没有转义)tinymoe"。一个用【R"<一些字符>(】开始的字符串只能由【)<一样的字符>"】来结束,要顺利分析这种状况,只能经过在状态机里面作hack才能解决。这就是为何咱们人肉构造词法分析器的时候,会把状态和动做都混在一块儿写,由于这样便于处理特殊状况。 单元测试

 

不过幸亏的是,Tinymoe并无这种状况发生。因此咱们能够直接从状态机入手。为了简单起见,我在下面的状态机中去掉全部不是+和-的符号。首先,咱们须要一个起始状态和一个结束状态:

 

首先咱们添加整数和标识符进去:

 

其次是加减和浮点:

 

最后把字符串和注释补全:

 

这样状态机就已经完成了。读过编译原理的可能会问,为何终结状态都是由虚线而不是带有输入的实现指向的?由于虚线在这里有特殊的意义,也就是说它不能移动输入的字符串的指针,并且他还要负责添加一个token。当状态跳到End以后,那他就会变成Start,因此实际上Start和End是同一个状态。这个状态机也不是输入什么字符都能跳转到下一个状态的。因此当你发现输入的字符让你无路可走的时候,你就是遇到了一个词法错误

 

这样咱们的设计就算完成了,接下来就是如何用C++来实现它了。为了让代码更容易阅读,咱们应该给Start和1-9这么多状态起名字,作法以下:

在这里相似状态3这样的状态被我省略掉了,由于这个状态惟一的出路就是虚线,因此跳到这个状态意味着你要马上执行虚线,也就是说你不须要作"跳到这个状态"这个动做。所以它不须要有一个名字。

 

而后你只要按照下面的作法翻译这个状态机就能够了:

 

只要写到这里,那么咱们就初步完成了词法分析器了。其实任何系统的主要功能都是相对容易实现的,每每是次要的功能才须要花费大量的精力来完成,并且还很容易出错。在这里"次要的功能"就是——记录token的行列号,还有维护CodeFile::lines避免空行的出现!

 

尽管我已经作过了那么屡次词法分析器,可是我仍然没法一鼓作气写对,仍然会出一些bug。面对编译器这种纯计算程序,debug的最好方法就是写单元测试。不过对于不熟悉单元测试的人来说可能很难一会儿想到要作什么测试,在这里我能够把我给Tinymoe谢的一部分单元测试在这里贴一下。

 

第一个,固然是传说中的"Hello, world!"测试了:

 

TEST_CASE和TEST_ASSERT(这里暂时没有直接用到TEST_ASSERT)是我为了开发Tinymoe随手撸的一个宏,能够在Tinymoe的代码里看到。为了检查咱们有没有粗枝大叶,咱们有必要检查词法分析器的任何一个输出的数据,譬如每一行有多少token,譬如每个token的行号列好内容和类型。我为了让这些枯燥的测试用例容易看懂,在这个文件(https://github.com/vczh/tinymoe/blob/master/Development/TinymoeUnitTest/TestLexicalAnalyzer.cpp)的开头能够看到FIRST_LINE、FIRST_TOKEN、TOKEN、LAST_TOKEN、NEXT_LINE、LAST_LINE是怎么定义的。其实吧这些宏展开,就是一个普通的遍历CodeFile::lines和CodeLine::tokens的程序,而后TEST_ASSERT一下CodeToken的每个成员是否咱们所须要的值。我相信看到这里不少人确定把重点放在宏和炫技上,而不是如何设计测试用例上,这是有前途的程序员和没前途的程序员面对一份资料的反应的重要区别之一。没前途的程序员老是会把注意力放在一些莫名其妙的地方,其中一个例子就是"过早优化"。

 

第二个测试用例针对的是整数和浮点的输出和报错上,重点在于检查每个token的列号是否是正确的计算了出来:

 

第三个测试用例的重点主要是-符号和—注释的分析:

 

第四个测试用例则是测试字符串的escaping和在面对换行的时候是否正确的处理(以前提到字符串不能换行,遇到一个忽然的换行将会被当作缺乏双引号):

 

鉴于词法分析原本内容也很少,因此这篇文章也不会太长。相信有前途的程序员也会在这里获得一些编译原理之外的知识。下一篇文章将会描述Tinymoe的函数头的语法分析部分,将会描述一个编译器的不带歧义的语法分析是如何人肉出来的。敬请期待。

相关文章
相关标签/搜索